@lodestar/beacon-node 1.43.0-dev.ff409c46ce → 1.43.0-rc.1

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 (54) hide show
  1. package/lib/chain/blocks/importExecutionPayload.d.ts +4 -0
  2. package/lib/chain/blocks/importExecutionPayload.d.ts.map +1 -1
  3. package/lib/chain/blocks/importExecutionPayload.js +12 -1
  4. package/lib/chain/blocks/importExecutionPayload.js.map +1 -1
  5. package/lib/chain/blocks/index.d.ts.map +1 -1
  6. package/lib/chain/blocks/index.js +6 -7
  7. package/lib/chain/blocks/index.js.map +1 -1
  8. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.d.ts +8 -1
  9. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.d.ts.map +1 -1
  10. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.js +19 -0
  11. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.js.map +1 -1
  12. package/lib/chain/blocks/payloadEnvelopeInput/types.d.ts +16 -0
  13. package/lib/chain/blocks/payloadEnvelopeInput/types.d.ts.map +1 -1
  14. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.d.ts.map +1 -1
  15. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js +5 -4
  16. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js.map +1 -1
  17. package/lib/chain/chain.d.ts.map +1 -1
  18. package/lib/chain/chain.js +15 -1
  19. package/lib/chain/chain.js.map +1 -1
  20. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts +7 -6
  21. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts.map +1 -1
  22. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js +21 -5
  23. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js.map +1 -1
  24. package/lib/sync/range/batch.d.ts +7 -3
  25. package/lib/sync/range/batch.d.ts.map +1 -1
  26. package/lib/sync/range/batch.js +96 -17
  27. package/lib/sync/range/batch.js.map +1 -1
  28. package/lib/sync/range/chain.d.ts +7 -2
  29. package/lib/sync/range/chain.d.ts.map +1 -1
  30. package/lib/sync/range/chain.js +11 -2
  31. package/lib/sync/range/chain.js.map +1 -1
  32. package/lib/sync/range/range.d.ts.map +1 -1
  33. package/lib/sync/range/range.js +14 -3
  34. package/lib/sync/range/range.js.map +1 -1
  35. package/lib/sync/sync.d.ts.map +1 -1
  36. package/lib/sync/sync.js +13 -0
  37. package/lib/sync/sync.js.map +1 -1
  38. package/lib/sync/utils/downloadByRange.d.ts +29 -8
  39. package/lib/sync/utils/downloadByRange.d.ts.map +1 -1
  40. package/lib/sync/utils/downloadByRange.js +85 -38
  41. package/lib/sync/utils/downloadByRange.js.map +1 -1
  42. package/package.json +15 -15
  43. package/src/chain/blocks/importExecutionPayload.ts +18 -6
  44. package/src/chain/blocks/index.ts +5 -7
  45. package/src/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.ts +27 -1
  46. package/src/chain/blocks/payloadEnvelopeInput/types.ts +17 -0
  47. package/src/chain/blocks/verifyExecutionPayloadEnvelope.ts +7 -4
  48. package/src/chain/chain.ts +17 -0
  49. package/src/chain/seenCache/seenPayloadEnvelopeInput.ts +23 -6
  50. package/src/sync/range/batch.ts +118 -20
  51. package/src/sync/range/chain.ts +20 -3
  52. package/src/sync/range/range.ts +16 -3
  53. package/src/sync/sync.ts +13 -1
  54. package/src/sync/utils/downloadByRange.ts +145 -39
@@ -1,18 +1,19 @@
1
1
  import {ChainForkConfig} from "@lodestar/config";
2
2
  import {ForkName, isForkPostDeneb, isForkPostFulu, isForkPostGloas} from "@lodestar/params";
3
- import {Epoch, RootHex, Slot, phase0} from "@lodestar/types";
4
- import {LodestarError, prettyPrintIndices} from "@lodestar/utils";
3
+ import {Epoch, RootHex, SignedBeaconBlock, Slot, gloas, phase0} from "@lodestar/types";
4
+ import {LodestarError, byteArrayEquals, prettyPrintIndices, toRootHex} from "@lodestar/utils";
5
5
  import {isBlockInputColumns} from "../../chain/blocks/blockInput/blockInput.js";
6
6
  import {IBlockInput} from "../../chain/blocks/blockInput/types.js";
7
7
  import {isDaOutOfRange} from "../../chain/blocks/blockInput/utils.js";
8
8
  import {PayloadEnvelopeInput} from "../../chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.js";
9
9
  import {BlockError, BlockErrorCode} from "../../chain/errors/index.js";
10
+ import {ZERO_HASH} from "../../constants/constants.js";
10
11
  import {PeerSyncMeta} from "../../network/peers/peersData.js";
11
12
  import {IClock} from "../../util/clock.js";
12
13
  import {CustodyConfig} from "../../util/dataColumns.js";
13
14
  import {PeerIdStr} from "../../util/peerId.js";
14
15
  import {MAX_BATCH_DOWNLOAD_ATTEMPTS, MAX_BATCH_PROCESSING_ATTEMPTS} from "../constants.js";
15
- import {DownloadByRangeRequests} from "../utils/downloadByRange.js";
16
+ import {DownloadByRangeRequests, ParentPayloadCommitments} from "../utils/downloadByRange.js";
16
17
  import {getBatchSlotRange, hashBlocks} from "./utils/index.js";
17
18
 
18
19
  /**
@@ -141,8 +142,18 @@ export class Batch {
141
142
  private readonly config: ChainForkConfig;
142
143
  private readonly clock: IClock;
143
144
  private readonly custodyConfig: CustodyConfig;
144
-
145
- constructor(startEpoch: Epoch, config: ChainForkConfig, clock: IClock, custodyConfig: CustodyConfig) {
145
+ private readonly isFirstBatchInChain: boolean;
146
+ private readonly latestBid: gloas.ExecutionPayloadBid | undefined;
147
+
148
+ constructor(
149
+ startEpoch: Epoch,
150
+ config: ChainForkConfig,
151
+ clock: IClock,
152
+ custodyConfig: CustodyConfig,
153
+ isFirstBatchInChain: boolean,
154
+ latestBid: gloas.ExecutionPayloadBid | undefined,
155
+ targetSlot: Slot
156
+ ) {
146
157
  this.config = config;
147
158
  this.clock = clock;
148
159
  this.custodyConfig = custodyConfig;
@@ -151,10 +162,40 @@ export class Batch {
151
162
  this.forkName = this.config.getForkName(startSlot);
152
163
  this.startEpoch = startEpoch;
153
164
  this.startSlot = startSlot;
154
- this.count = count;
165
+ this.count = Math.min(count, targetSlot - startSlot + 1);
166
+ this.isFirstBatchInChain = isFirstBatchInChain;
167
+ this.latestBid = latestBid;
155
168
  this.requests = this.getRequests([]);
156
169
  }
157
170
 
171
+ private shouldDownloadParentEnvelope(firstBlock?: SignedBeaconBlock): boolean {
172
+ if (!this.isFirstBatchInChain) return false;
173
+
174
+ if (this.startSlot === 0 || !isForkPostGloas(this.config.getForkName(this.startSlot - 1))) {
175
+ return false;
176
+ }
177
+
178
+ // we only know if we should download parent envelope if firstBlock is downloaded
179
+ if (firstBlock === undefined) return false;
180
+ if (this.latestBid === undefined) return false;
181
+ const firstBlockBidParentHash = (firstBlock.message.body as gloas.BeaconBlockBody).signedExecutionPayloadBid.message
182
+ .parentBlockHash;
183
+ return byteArrayEquals(firstBlockBidParentHash, this.latestBid.blockHash);
184
+ }
185
+
186
+ getParentPayloadCommitments(parentBlockRoot: Uint8Array): ParentPayloadCommitments {
187
+ if (this.latestBid === undefined) {
188
+ throw new Error(
189
+ `Coding error: getParentPayloadCommitments called without latestBid for parentBlockRoot=${toRootHex(parentBlockRoot)}`
190
+ );
191
+ }
192
+ return {
193
+ blockRoot: parentBlockRoot,
194
+ blockRootHex: toRootHex(parentBlockRoot),
195
+ kzgCommitments: this.latestBid.blobKzgCommitments,
196
+ };
197
+ }
198
+
158
199
  /**
159
200
  * Builds ByRange requests for block, blobs and columns
160
201
  */
@@ -292,6 +333,36 @@ export class Batch {
292
333
  };
293
334
  }
294
335
 
336
+ // Only the first batch of a SyncChain may need the dangling-parent payload by-root.
337
+ if (blocks.length > 0 && this.shouldDownloadParentEnvelope(blocks[0].getBlock())) {
338
+ // shouldDownloadParentEnvelope() = true means there are at least 1 block
339
+ const parentRoot = blocks[0].getBlock().message.parentRoot;
340
+ if (!byteArrayEquals(parentRoot, ZERO_HASH)) {
341
+ const parentRootHex = toRootHex(parentRoot);
342
+ let parentPayloadInput: PayloadEnvelopeInput | undefined;
343
+ if (this.state.payloadEnvelopes) {
344
+ for (const pi of this.state.payloadEnvelopes.values()) {
345
+ if (pi.blockRootHex === parentRootHex) {
346
+ parentPayloadInput = pi;
347
+ break;
348
+ }
349
+ }
350
+ }
351
+
352
+ const needsEnvelope = !parentPayloadInput?.hasPayloadEnvelope();
353
+ const missingColumns = parentPayloadInput
354
+ ? parentPayloadInput.getMissingSampledColumnMeta().missing
355
+ : this.custodyConfig.sampledColumns;
356
+
357
+ if (needsEnvelope || missingColumns.length > 0) {
358
+ requests.parentPayloadRequest = {
359
+ ...(needsEnvelope ? {envelopeBlockRoot: parentRoot} : {}),
360
+ ...(missingColumns.length > 0 ? {blockRoot: parentRoot, columns: missingColumns} : {}),
361
+ };
362
+ }
363
+ }
364
+ }
365
+
295
366
  return requests;
296
367
  }
297
368
 
@@ -303,24 +374,28 @@ export class Batch {
303
374
  return this.requests;
304
375
  }
305
376
 
306
- // post-fulu we need to ensure that we only request columns that the peer has advertised
307
- const {columnsRequest} = this.requests;
308
- if (columnsRequest == null) {
309
- return this.requests;
310
- }
377
+ // post-fulu we need to ensure that we only request columns that the peer has advertised.
378
+ const {columnsRequest, parentPayloadRequest} = this.requests;
311
379
 
312
380
  const peerColumns = new Set(peer.custodyColumns ?? []);
313
- const requestedColumns = columnsRequest.columns.filter((c) => peerColumns.has(c));
314
- if (requestedColumns.length === columnsRequest.columns.length) {
315
- return this.requests;
316
- }
381
+ const filteredColumnsRequest =
382
+ columnsRequest != null ? columnsRequest.columns.filter((c) => peerColumns.has(c)) : null;
383
+ const parentColumns = parentPayloadRequest?.columns;
384
+ const filteredParentColumns = parentColumns != null ? parentColumns.filter((c) => peerColumns.has(c)) : null;
385
+
386
+ const updatedColumnRequest =
387
+ columnsRequest != null && filteredColumnsRequest != null
388
+ ? {columnsRequest: {...columnsRequest, columns: filteredColumnsRequest}}
389
+ : {};
390
+ const updatedParentPayloadRequest =
391
+ parentPayloadRequest != null && filteredParentColumns != null
392
+ ? {parentPayloadRequest: {...parentPayloadRequest, columns: filteredParentColumns}}
393
+ : {};
317
394
 
318
395
  return {
319
396
  ...this.requests,
320
- columnsRequest: {
321
- ...columnsRequest,
322
- columns: requestedColumns,
323
- },
397
+ ...updatedColumnRequest,
398
+ ...updatedParentPayloadRequest,
324
399
  };
325
400
  }
326
401
 
@@ -422,7 +497,7 @@ export class Batch {
422
497
  if (allComplete && isForkPostGloas(this.forkName)) {
423
498
  for (const block of blocks) {
424
499
  const payloadInput = newPayloadEnvelopes?.get(block.slot);
425
- // by_range needs to download all columns
500
+ // by_range needs every block's envelope and all sampled columns.
426
501
  if (!payloadInput?.hasPayloadEnvelope() || !payloadInput.hasComputedAllData()) {
427
502
  allComplete = false;
428
503
  break;
@@ -430,6 +505,29 @@ export class Batch {
430
505
  }
431
506
  }
432
507
 
508
+ // First batch of a sync chain must additionally have the dangling-parent payload fully
509
+ // present, otherwise `processBlocks` will throw PARENT_PAYLOAD_UNKNOWN. The parent's
510
+ // `PayloadEnvelopeInput` is identified by `blockRootHex` matching `blocks[0].parentRoot`.
511
+ if (allComplete && blocks.length > 0 && this.shouldDownloadParentEnvelope(blocks[0].getBlock())) {
512
+ const parentRoot = blocks[0].getBlock().message.parentRoot;
513
+ // Genesis has no parent payload — nothing to wait for.
514
+ if (!byteArrayEquals(parentRoot, ZERO_HASH)) {
515
+ const parentRootHex = toRootHex(parentRoot);
516
+ let parentPayloadComplete = false;
517
+ if (newPayloadEnvelopes) {
518
+ for (const payloadInput of newPayloadEnvelopes.values()) {
519
+ if (payloadInput.blockRootHex === parentRootHex) {
520
+ parentPayloadComplete = payloadInput.hasPayloadEnvelope() && payloadInput.hasComputedAllData();
521
+ break;
522
+ }
523
+ }
524
+ }
525
+ if (!parentPayloadComplete) {
526
+ allComplete = false;
527
+ }
528
+ }
529
+ }
530
+
433
531
  if (allComplete) {
434
532
  this.state = {status: BatchStatus.AwaitingProcessing, blocks, payloadEnvelopes: newPayloadEnvelopes};
435
533
  } else {
@@ -1,6 +1,6 @@
1
1
  import {ChainForkConfig} from "@lodestar/config";
2
2
  import {RequestErrorCode} from "@lodestar/reqresp";
3
- import {Epoch, Root, Slot} from "@lodestar/types";
3
+ import {Epoch, Root, Slot, gloas} from "@lodestar/types";
4
4
  import {ErrorAborted, LodestarError, Logger, prettyPrintIndices, toRootHex} from "@lodestar/utils";
5
5
  import {isBlockInputBlobs, isBlockInputColumns} from "../../chain/blocks/blockInput/blockInput.js";
6
6
  import {BlockInputErrorCode} from "../../chain/blocks/blockInput/errors.js";
@@ -145,6 +145,10 @@ export class SyncChain {
145
145
  private readonly batchProcessor = new ItTrigger();
146
146
  /** Sorted map of batches undergoing some kind of processing. */
147
147
  private readonly batches = new Map<Epoch, Batch>();
148
+ /**
149
+ * `true` until the first `Batch` is constructed via `includeNextBatch`
150
+ */
151
+ private isFirstBatch = true;
148
152
  private readonly peerset = new Map<PeerIdStr, ChainTarget>();
149
153
  /**
150
154
  * Tracks peers that have rate-limited us, mapped to the timestamp (ms) until which we should avoid them.
@@ -158,13 +162,15 @@ export class SyncChain {
158
162
  private readonly clock: IClock;
159
163
  private readonly metrics: Metrics | null;
160
164
  private readonly custodyConfig: CustodyConfig;
165
+ private readonly latestBid: gloas.ExecutionPayloadBid | undefined;
161
166
 
162
167
  constructor(
163
168
  initialBatchEpoch: Epoch,
164
169
  initialTarget: ChainTarget,
165
170
  syncType: RangeSyncType,
166
171
  fns: SyncChainFns,
167
- modules: SyncChainModules
172
+ modules: SyncChainModules,
173
+ latestBid: gloas.ExecutionPayloadBid | undefined
168
174
  ) {
169
175
  const {config, clock, custodyConfig, logger, metrics} = modules;
170
176
  this.firstBatchEpoch = initialBatchEpoch;
@@ -180,6 +186,7 @@ export class SyncChain {
180
186
  this.clock = clock;
181
187
  this.metrics = metrics;
182
188
  this.custodyConfig = custodyConfig;
189
+ this.latestBid = latestBid;
183
190
  this.logger = logger;
184
191
  this.logId = `${syncType}-${nextChainId++}`;
185
192
 
@@ -481,7 +488,17 @@ export class SyncChain {
481
488
  return null;
482
489
  }
483
490
 
484
- const batch = new Batch(startEpoch, this.config, this.clock, this.custodyConfig);
491
+ const batch = new Batch(
492
+ startEpoch,
493
+ this.config,
494
+ this.clock,
495
+ this.custodyConfig,
496
+ this.isFirstBatch,
497
+ // `latestBid` is only meaningful for the first batch's parent-payload check
498
+ this.isFirstBatch ? this.latestBid : undefined,
499
+ this.target.slot
500
+ );
501
+ this.isFirstBatch = false;
485
502
  this.batches.set(startEpoch, batch);
486
503
  return batch;
487
504
  }
@@ -1,7 +1,7 @@
1
1
  import {EventEmitter} from "node:events";
2
2
  import {StrictEventEmitter} from "strict-event-emitter-types";
3
3
  import {BeaconConfig} from "@lodestar/config";
4
- import {computeStartSlotAtEpoch} from "@lodestar/state-transition";
4
+ import {IBeaconStateViewGloas, computeStartSlotAtEpoch, isStatePostGloas} from "@lodestar/state-transition";
5
5
  import {Epoch, Status, fulu} from "@lodestar/types";
6
6
  import {Logger, toRootHex} from "@lodestar/utils";
7
7
  import {IBlockInput} from "../../chain/blocks/blockInput/types.js";
@@ -206,14 +206,18 @@ export class RangeSync extends (EventEmitter as {new (): RangeSyncEmitter}) {
206
206
 
207
207
  private downloadByRange: SyncChainFns["downloadByRange"] = async (peer, batch) => {
208
208
  const batchBlocks = batch.getBlocks();
209
+ const requests = batch.getRequestsForPeer(peer);
210
+ const parentRoot = requests.parentPayloadRequest?.envelopeBlockRoot ?? requests.parentPayloadRequest?.blockRoot;
211
+ const parentPayloadCommitments = parentRoot ? batch.getParentPayloadCommitments(parentRoot) : undefined;
209
212
  const {result, warnings} = await downloadByRange({
210
213
  config: this.config,
211
214
  network: this.network,
212
215
  logger: this.logger,
213
216
  peerIdStr: peer.peerId,
214
217
  batchBlocks,
218
+ parentPayloadCommitments,
215
219
  peerDasMetrics: this.chain.metrics?.peerDas,
216
- ...batch.getRequestsForPeer(peer),
220
+ ...requests,
217
221
  });
218
222
  const {responses, payloadEnvelopes: downloadedPayloadEnvelopes} = result;
219
223
  const {blocks, payloadEnvelopes} = cacheByRangeResponses({
@@ -258,6 +262,14 @@ export class RangeSync extends (EventEmitter as {new (): RangeSyncEmitter}) {
258
262
  private addPeerOrCreateChain(startEpoch: Epoch, target: ChainTarget, peer: PeerIdStr, syncType: RangeSyncType): void {
259
263
  let syncChain = this.chains.get(syncType);
260
264
  if (!syncChain) {
265
+ // The first batch of a new sync chain may need to detect whether the parent block was an
266
+ // gloas "empty" block (no envelope produced). It does so by comparing the first
267
+ // downloaded block's `bid.parentBlockHash` against the head state's `latestExecutionPayloadBid.blockHash`.
268
+ const headState = this.chain.getHeadState();
269
+ const latestBid = isStatePostGloas(headState)
270
+ ? (headState as IBeaconStateViewGloas).latestExecutionPayloadBid
271
+ : undefined;
272
+
261
273
  syncChain = new SyncChain(
262
274
  startEpoch,
263
275
  target,
@@ -276,7 +288,8 @@ export class RangeSync extends (EventEmitter as {new (): RangeSyncEmitter}) {
276
288
  logger: this.logger,
277
289
  custodyConfig: this.chain.custodyConfig,
278
290
  metrics: this.metrics,
279
- }
291
+ },
292
+ latestBid
280
293
  );
281
294
  this.chains.set(syncType, syncChain);
282
295
 
package/src/sync/sync.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import {SLOTS_PER_EPOCH} from "@lodestar/params";
2
2
  import {Slot} from "@lodestar/types";
3
- import {Logger} from "@lodestar/utils";
3
+ import {Logger, toRootHex} from "@lodestar/utils";
4
4
  import {IBeaconChain} from "../chain/index.js";
5
5
  import {GENESIS_SLOT} from "../constants/constants.js";
6
6
  import {ExecutionEngineState} from "../execution/index.js";
@@ -188,6 +188,18 @@ export class BeaconSync implements IBeaconSync {
188
188
  private addPeer = (data: NetworkEventData[NetworkEvent.peerConnected]): void => {
189
189
  const localStatus = this.chain.getStatus();
190
190
  const syncType = getPeerSyncType(localStatus, data.status, this.chain.forkChoice, this.slotImportTolerance);
191
+ this.logger.verbose("Peer sync type classified", {
192
+ peer: data.peer,
193
+ syncType,
194
+ localFinalizedEpoch: localStatus.finalizedEpoch,
195
+ localFinalizedRoot: toRootHex(localStatus.finalizedRoot),
196
+ localHeadSlot: localStatus.headSlot,
197
+ localHeadRoot: toRootHex(localStatus.headRoot),
198
+ remoteFinalizedEpoch: data.status.finalizedEpoch,
199
+ remoteFinalizedRoot: toRootHex(data.status.finalizedRoot),
200
+ remoteHeadSlot: data.status.headSlot,
201
+ remoteHeadRoot: toRootHex(data.status.headRoot),
202
+ });
191
203
 
192
204
  // For metrics only
193
205
  this.peerSyncType.set(data.peer, syncType);