@lodestar/beacon-node 1.50.0-dev.cc4428df4b → 1.50.0-dev.cea8fba405

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.
Files changed (32) hide show
  1. package/lib/db/beacon.d.ts.map +1 -1
  2. package/lib/db/beacon.js +2 -0
  3. package/lib/db/beacon.js.map +1 -1
  4. package/lib/execution/engine/http.d.ts.map +1 -1
  5. package/lib/execution/engine/http.js +5 -4
  6. package/lib/execution/engine/http.js.map +1 -1
  7. package/lib/metrics/metrics/beacon.d.ts +1 -1
  8. package/lib/metrics/metrics/beacon.d.ts.map +1 -1
  9. package/lib/metrics/metrics/beacon.js.map +1 -1
  10. package/lib/network/metadata.d.ts +1 -1
  11. package/lib/network/metadata.d.ts.map +1 -1
  12. package/lib/network/metadata.js +17 -17
  13. package/lib/network/metadata.js.map +1 -1
  14. package/lib/network/peers/datastore.d.ts +9 -9
  15. package/lib/network/peers/datastore.d.ts.map +1 -1
  16. package/lib/network/peers/datastore.js +36 -36
  17. package/lib/network/peers/datastore.js.map +1 -1
  18. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRoot.d.ts.map +1 -1
  19. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRoot.js +41 -26
  20. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRoot.js.map +1 -1
  21. package/lib/util/execution.d.ts +2 -0
  22. package/lib/util/execution.d.ts.map +1 -1
  23. package/lib/util/execution.js +6 -6
  24. package/lib/util/execution.js.map +1 -1
  25. package/package.json +15 -15
  26. package/src/db/beacon.ts +2 -0
  27. package/src/execution/engine/http.ts +6 -4
  28. package/src/metrics/metrics/beacon.ts +1 -1
  29. package/src/network/metadata.ts +17 -17
  30. package/src/network/peers/datastore.ts +36 -36
  31. package/src/network/reqresp/handlers/executionPayloadEnvelopesByRoot.ts +42 -28
  32. package/src/util/execution.ts +10 -6
@@ -16,23 +16,23 @@ type AwaitGenerator<T, TReturn = any, TNext = any> = Generator<T, TReturn, TNext
16
16
  * This has a memory issue because all peer data stays in memory and loaded at startup time
17
17
  * This is written for libp2p >=0.35, we maintain the same mechanism but with bounded data structure
18
18
  * This datastore includes a memory datastore and fallback to db datastore
19
- * Use an in-memory datastore with last accessed time and _maxMemoryItems, on start it's empty (lazy load)
19
+ * Use an in-memory datastore with last accessed time and maxMemoryItems, on start it's empty (lazy load)
20
20
  * - get: Search in-memory datastore first, if not found search from db.
21
21
  * - If found from db, add back to the in-memory datastore
22
22
  * - Update lastAccessedMs
23
- * - put: move oldest items from memory to db if there are more than _maxMemoryItems items in memory
24
- * - update memory datastore, only update db datastore if there are at least _threshold dirty items
23
+ * - put: move oldest items from memory to db if there are more than maxMemoryItems items in memory
24
+ * - update memory datastore, only update db datastore if there are at least threshold dirty items
25
25
  * - Update lastAccessedMs
26
26
  */
27
27
  export class Eth2PeerDataStore extends BaseDatastore {
28
- private _dbDatastore: LevelDatastore;
29
- private _memoryDatastore: Map<string, MemoryItem>;
28
+ private dbDatastore: LevelDatastore;
29
+ private memoryDatastore: Map<string, MemoryItem>;
30
30
  /** Same to PersistentPeerStore of the old libp2p implementation */
31
- private _dirtyItems = new Set<string>();
31
+ private dirtyItems = new Set<string>();
32
32
  /** If there are more dirty items than threshold, commit data to db */
33
- private _threshold: number;
33
+ private threshold: number;
34
34
  /** If there are more memory items than this, prune oldest ones from memory and move to db */
35
- private _maxMemoryItems: number;
35
+ private maxMemoryItems: number;
36
36
 
37
37
  constructor(
38
38
  dbDatastore: LevelDatastore | string,
@@ -47,18 +47,18 @@ export class Eth2PeerDataStore extends BaseDatastore {
47
47
  throw Error(`Threshold ${threshold} should be at most maxMemoryItems ${maxMemoryItems}`);
48
48
  }
49
49
 
50
- this._dbDatastore = typeof dbDatastore === "string" ? new LevelDatastore(dbDatastore) : dbDatastore;
51
- this._memoryDatastore = new Map();
52
- this._threshold = threshold;
53
- this._maxMemoryItems = maxMemoryItems;
50
+ this.dbDatastore = typeof dbDatastore === "string" ? new LevelDatastore(dbDatastore) : dbDatastore;
51
+ this.memoryDatastore = new Map();
52
+ this.threshold = threshold;
53
+ this.maxMemoryItems = maxMemoryItems;
54
54
  }
55
55
 
56
56
  async open(): Promise<void> {
57
- return this._dbDatastore.open();
57
+ return this.dbDatastore.open();
58
58
  }
59
59
 
60
60
  async close(): Promise<void> {
61
- return this._dbDatastore.close();
61
+ return this.dbDatastore.close();
62
62
  }
63
63
 
64
64
  async put(key: Key, val: Uint8Array, _options?: AbortOptions): Promise<Key> {
@@ -67,23 +67,23 @@ export class Eth2PeerDataStore extends BaseDatastore {
67
67
 
68
68
  /**
69
69
  * Same interface to put with "fromDb" option, if this item is updated back from db
70
- * Move oldest items from memory data store to db if it's over this._maxMemoryItems
70
+ * Move oldest items from memory data store to db if it's over this.maxMemoryItems
71
71
  */
72
72
  async _put(key: Key, val: Uint8Array, fromDb = false): Promise<Key> {
73
- while (this._memoryDatastore.size >= this._maxMemoryItems) {
73
+ while (this.memoryDatastore.size >= this.maxMemoryItems) {
74
74
  // it's likely this is called only 1 time
75
75
  await this.pruneMemoryDatastore();
76
76
  }
77
77
 
78
78
  const keyStr = key.toString();
79
- const memoryItem = this._memoryDatastore.get(keyStr);
79
+ const memoryItem = this.memoryDatastore.get(keyStr);
80
80
  if (memoryItem) {
81
81
  // update existing
82
82
  memoryItem.lastAccessedMs = Date.now();
83
83
  memoryItem.data = val;
84
84
  } else {
85
85
  // new
86
- this._memoryDatastore.set(keyStr, {data: val, lastAccessedMs: Date.now()});
86
+ this.memoryDatastore.set(keyStr, {data: val, lastAccessedMs: Date.now()});
87
87
  }
88
88
 
89
89
  if (!fromDb) await this._addDirtyItem(keyStr);
@@ -98,15 +98,15 @@ export class Eth2PeerDataStore extends BaseDatastore {
98
98
  */
99
99
  async get(key: Key, options?: AbortOptions): Promise<Uint8Array> {
100
100
  const keyStr = key.toString();
101
- const memoryItem = this._memoryDatastore.get(keyStr);
101
+ const memoryItem = this.memoryDatastore.get(keyStr);
102
102
  if (memoryItem) {
103
103
  memoryItem.lastAccessedMs = Date.now();
104
104
  return memoryItem.data;
105
105
  }
106
106
 
107
107
  // this throws error if not found
108
- const dbValue = await this._dbDatastore.get(key, options);
109
- // don't call this._memoryDatastore.set directly
108
+ const dbValue = await this.dbDatastore.get(key, options);
109
+ // don't call this.memoryDatastore.set directly
110
110
  // we want to get through prune() logic with fromDb as true
111
111
  await this._put(key, dbValue, true);
112
112
  return dbValue;
@@ -125,30 +125,30 @@ export class Eth2PeerDataStore extends BaseDatastore {
125
125
  }
126
126
 
127
127
  async delete(key: Key, options?: AbortOptions): Promise<void> {
128
- this._memoryDatastore.delete(key.toString());
129
- await this._dbDatastore.delete(key, options);
128
+ this.memoryDatastore.delete(key.toString());
129
+ await this.dbDatastore.delete(key, options);
130
130
  }
131
131
 
132
132
  async *_all(q: Query, options?: AbortOptions): AwaitGenerator<Pair> {
133
- for (const [key, value] of this._memoryDatastore.entries()) {
133
+ for (const [key, value] of this.memoryDatastore.entries()) {
134
134
  yield {
135
135
  key: new Key(key),
136
136
  value: value.data,
137
137
  };
138
138
  }
139
- yield* this._dbDatastore.query(q, options);
139
+ yield* this.dbDatastore.query(q, options);
140
140
  }
141
141
 
142
142
  async *_allKeys(q: KeyQuery, options?: AbortOptions): AwaitGenerator<Key> {
143
- for (const key of this._memoryDatastore.keys()) {
143
+ for (const key of this.memoryDatastore.keys()) {
144
144
  yield new Key(key);
145
145
  }
146
- yield* this._dbDatastore.queryKeys(q, options);
146
+ yield* this.dbDatastore.queryKeys(q, options);
147
147
  }
148
148
 
149
149
  private async _addDirtyItem(keyStr: string): Promise<void> {
150
- this._dirtyItems.add(keyStr);
151
- if (this._dirtyItems.size >= this._threshold) {
150
+ this.dirtyItems.add(keyStr);
151
+ if (this.dirtyItems.size >= this.threshold) {
152
152
  try {
153
153
  await this._commitData();
154
154
  } catch (_e) {}
@@ -156,15 +156,15 @@ export class Eth2PeerDataStore extends BaseDatastore {
156
156
  }
157
157
 
158
158
  private async _commitData(): Promise<void> {
159
- const batch = this._dbDatastore.batch();
160
- for (const keyStr of this._dirtyItems) {
161
- const memoryItem = this._memoryDatastore.get(keyStr);
159
+ const batch = this.dbDatastore.batch();
160
+ for (const keyStr of this.dirtyItems) {
161
+ const memoryItem = this.memoryDatastore.get(keyStr);
162
162
  if (memoryItem) {
163
163
  batch.put(new Key(keyStr), memoryItem.data);
164
164
  }
165
165
  }
166
166
  await batch.commit();
167
- this._dirtyItems.clear();
167
+ this.dirtyItems.clear();
168
168
  }
169
169
 
170
170
  /**
@@ -175,7 +175,7 @@ export class Eth2PeerDataStore extends BaseDatastore {
175
175
  let oldestKey: string | undefined = undefined;
176
176
  let oldestValue: Uint8Array | undefined = undefined;
177
177
 
178
- for (const [key, value] of this._memoryDatastore) {
178
+ for (const [key, value] of this.memoryDatastore) {
179
179
  if (value.lastAccessedMs < oldestAccessedMs) {
180
180
  oldestAccessedMs = value.lastAccessedMs;
181
181
  oldestKey = key;
@@ -184,8 +184,8 @@ export class Eth2PeerDataStore extends BaseDatastore {
184
184
  }
185
185
 
186
186
  if (oldestKey && oldestValue) {
187
- await this._dbDatastore.put(new Key(oldestKey), oldestValue);
188
- this._memoryDatastore.delete(oldestKey);
187
+ await this.dbDatastore.put(new Key(oldestKey), oldestValue);
188
+ this.memoryDatastore.delete(oldestKey);
189
189
  }
190
190
  }
191
191
  }
@@ -6,6 +6,7 @@ import {toRootHex} from "@lodestar/utils";
6
6
  import {EnvelopeReconstructionError} from "../../../chain/errors/index.js";
7
7
  import {IBeaconChain} from "../../../chain/index.js";
8
8
  import {IBeaconDb} from "../../../db/index.js";
9
+ import {MAX_BODIES_PER_REQUEST} from "../../../util/execution.js";
9
10
  import {ExecutionPayloadEnvelopesByRootRequest} from "../../../util/types.js";
10
11
  import {prettyPrintPeerId} from "../../util.js";
11
12
 
@@ -19,10 +20,15 @@ export async function* onExecutionPayloadEnvelopesByRoot(
19
20
  // The gloas req/resp spec uses MIN_EPOCHS_FOR_BLOCK_REQUESTS to define the minimum range peers MUST serve.
20
21
  // Archival nodes may still serve older retained payloads to allow genesis sync.
21
22
 
22
- // Resolve slots first so archived header envelopes are rebuilt in one EL batch, not one call per root
23
+ // Resolve slots first so archived envelopes can be reconstructed in EL batches.
24
+ // Duplicate roots are served once, since each archived envelope costs an EL fetch and a rebuild.
23
25
  const requests: {blockSlot: Slot; blockRootHex: RootHex}[] = [];
26
+ const seenRoots = new Set<RootHex>();
24
27
  for (const root of requestBody) {
25
28
  const rootHex = toRootHex(root);
29
+ if (seenRoots.has(rootHex)) continue;
30
+ seenRoots.add(rootHex);
31
+
26
32
  const block = chain.forkChoice.getBlockHexDefaultStatus(rootHex);
27
33
  // If the block is not in fork choice, it may be finalized. Attempt to find its slot in block archive
28
34
  const slot = block ? block.slot : await db.blockArchive.getSlotByRoot(root);
@@ -41,35 +47,43 @@ export async function* onExecutionPayloadEnvelopesByRoot(
41
47
  requests.push({blockSlot: slot, blockRootHex: rootHex});
42
48
  }
43
49
 
44
- let envelopesBytes: (Uint8Array | null)[];
45
- try {
46
- // by-root allows omission, so a mismatched envelope is left out rather than failing the response
47
- envelopesBytes = await chain.getSerializedExecutionPayloadEnvelopes(requests, "omit");
48
- } catch (e) {
49
- if (e instanceof EnvelopeReconstructionError) {
50
- throw new ResponseError(
51
- RespStatus.RESOURCE_UNAVAILABLE,
52
- `Failed to reconstruct archived envelopes: ${e.message}`
53
- );
50
+ // Yield between EL batches so the first response does not wait for the entire request.
51
+ let yielded = 0;
52
+ for (let i = 0; i < requests.length; i += MAX_BODIES_PER_REQUEST) {
53
+ const batch = requests.slice(i, i + MAX_BODIES_PER_REQUEST);
54
+ let envelopesBytes: (Uint8Array | null)[];
55
+ try {
56
+ // By-root allows omission, so a mismatched envelope is left out rather than failing the response.
57
+ envelopesBytes = await chain.getSerializedExecutionPayloadEnvelopes(batch, "omit");
58
+ } catch (e) {
59
+ if (e instanceof EnvelopeReconstructionError) {
60
+ // By-root permits partial responses, so an EL failure can end the response here.
61
+ if (yielded > 0) return;
62
+ throw new ResponseError(
63
+ RespStatus.RESOURCE_UNAVAILABLE,
64
+ `Failed to reconstruct archived envelopes: ${e.message}`
65
+ );
66
+ }
67
+ throw e;
54
68
  }
55
- throw e;
56
- }
57
69
 
58
- for (let i = 0; i < requests.length; i++) {
59
- const {blockSlot, blockRootHex} = requests[i];
60
- const envelopeBytes = envelopesBytes[i];
61
- if (envelopeBytes) {
62
- yield {
63
- data: envelopeBytes,
64
- boundary: chain.config.getForkBoundaryAtEpoch(computeEpochAtSlot(blockSlot)),
65
- };
66
- } else {
67
- chain.logger.debug("Cannot serve ExecutionPayloadEnvelopesByRoot: envelope not found", {
68
- slot: blockSlot,
69
- root: blockRootHex,
70
- peer: prettyPrintPeerId(peerId),
71
- client: peerClient,
72
- });
70
+ for (let j = 0; j < batch.length; j++) {
71
+ const {blockSlot, blockRootHex} = batch[j];
72
+ const envelopeBytes = envelopesBytes[j];
73
+ if (envelopeBytes) {
74
+ yielded++;
75
+ yield {
76
+ data: envelopeBytes,
77
+ boundary: chain.config.getForkBoundaryAtEpoch(computeEpochAtSlot(blockSlot)),
78
+ };
79
+ } else {
80
+ chain.logger.debug("Cannot serve ExecutionPayloadEnvelopesByRoot: envelope not found", {
81
+ slot: blockSlot,
82
+ root: blockRootHex,
83
+ peer: prettyPrintPeerId(peerId),
84
+ client: peerClient,
85
+ });
86
+ }
73
87
  }
74
88
  }
75
89
  }
@@ -259,8 +259,8 @@ export async function getDataColumnSidecarsFromExecution(
259
259
  return DataColumnEngineResult.SuccessResolved;
260
260
  }
261
261
 
262
- /** engine_getPayloadBodiesByHashV2: ELs MUST support at least 32 hashes per request. */
263
- const MAX_BODIES_REQUEST = 32;
262
+ /** engine_getPayloadBodiesByHashV2: every EL must accept requests of up to 32 hashes, larger ones may fail with -38004 */
263
+ export const MAX_BODIES_PER_REQUEST = 32;
264
264
 
265
265
  type SlotEnvelopeBytes = {slot: Slot; envelopeBytes: Uint8Array};
266
266
 
@@ -299,7 +299,7 @@ export async function* reconstructExecutionPayloadEnvelopesByRange(
299
299
 
300
300
  for await (const {key, value: bytes} of archive.binaryEntriesStream({gte: startSlot, lt: endSlot})) {
301
301
  batch.push({slot: archive.decodeKey(key), ...decodeArchivedEnvelope(bytes)});
302
- if (batch.length === MAX_BODIES_REQUEST) {
302
+ if (batch.length === MAX_BODIES_PER_REQUEST) {
303
303
  yield* reconstructBatch(executionEngine, logger, metrics, batch);
304
304
  batch = [];
305
305
  }
@@ -359,9 +359,13 @@ export async function reconstructExecutionPayloadEnvelopes(
359
359
  headerEnvelopes: gloas.SignedExecutionPayloadHeaderEnvelope[]
360
360
  ): Promise<(gloas.SignedExecutionPayloadEnvelope | RebuildMiss)[]> {
361
361
  const out: (gloas.SignedExecutionPayloadEnvelope | RebuildMiss)[] = [];
362
- for (let i = 0; i < headerEnvelopes.length; i += MAX_BODIES_REQUEST) {
362
+ for (let i = 0; i < headerEnvelopes.length; i += MAX_BODIES_PER_REQUEST) {
363
363
  out.push(
364
- ...(await reconstructEnvelopesBatch(executionEngine, metrics, headerEnvelopes.slice(i, i + MAX_BODIES_REQUEST)))
364
+ ...(await reconstructEnvelopesBatch(
365
+ executionEngine,
366
+ metrics,
367
+ headerEnvelopes.slice(i, i + MAX_BODIES_PER_REQUEST)
368
+ ))
365
369
  );
366
370
  }
367
371
  return out;
@@ -409,7 +413,7 @@ async function reconstructEnvelopesBatch(
409
413
  withdrawals: body.withdrawals,
410
414
  blockAccessList: body.blockAccessList,
411
415
  });
412
- metrics?.payloadEnvelopeReconstruction.envelopes.inc({result: "ok"});
416
+ metrics?.payloadEnvelopeReconstruction.envelopes.inc({result: "success"});
413
417
  return envelope;
414
418
  } catch (e) {
415
419
  if (