@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
@@ -8,13 +8,14 @@ import {
8
8
  isForkPostGloas,
9
9
  } from "@lodestar/params";
10
10
  import {
11
+ ColumnIndex,
11
12
  DataColumnSidecar,
13
+ RootHex,
12
14
  SignedBeaconBlock,
13
15
  Slot,
14
16
  deneb,
15
17
  fulu,
16
18
  gloas,
17
- isGloasBeaconBlock,
18
19
  isGloasDataColumnSidecar,
19
20
  phase0,
20
21
  } from "@lodestar/types";
@@ -46,6 +47,22 @@ export type DownloadByRangeRequests = {
46
47
  blobsRequest?: deneb.BlobSidecarsByRangeRequest;
47
48
  columnsRequest?: fulu.DataColumnSidecarsByRangeRequest;
48
49
  envelopesRequest?: gloas.ExecutionPayloadEnvelopesByRangeRequest;
50
+ /**
51
+ * Post-Gloas only. Fetches the dangling-parent's payload envelope and/or its missing sampled
52
+ * data columns by-root. Set by `Batch` for the first batch of a `SyncChain` after the first
53
+ * block's parentRoot is known.
54
+ */
55
+ parentPayloadRequest?: {
56
+ blockRoot?: Uint8Array;
57
+ columns?: ColumnIndex[];
58
+ envelopeBlockRoot?: Uint8Array;
59
+ };
60
+ };
61
+
62
+ export type ParentPayloadCommitments = {
63
+ blockRoot: Uint8Array;
64
+ blockRootHex: RootHex;
65
+ kzgCommitments: deneb.BlobKzgCommitments;
49
66
  };
50
67
 
51
68
  export type DownloadByRangeResponses = {
@@ -61,6 +78,8 @@ export type DownloadAndCacheByRangeProps = DownloadByRangeRequests & {
61
78
  logger: Logger;
62
79
  peerIdStr: string;
63
80
  batchBlocks?: IBlockInput[];
81
+ /** Required when `parentPayloadRequest` is set; supplies the data needed to validate the parent's columns. */
82
+ parentPayloadCommitments?: ParentPayloadCommitments;
64
83
  peerDasMetrics?: BeaconMetrics["peerDas"] | null;
65
84
  };
66
85
 
@@ -207,16 +226,12 @@ export function cacheByRangeResponses({
207
226
  if (downloadedPayloadEnvelopes !== null) {
208
227
  payloadEnvelopes ??= new Map();
209
228
  for (const [slot, envelope] of downloadedPayloadEnvelopes) {
210
- const blockInput = updatedBatchBlocks.get(slot);
211
- if (!blockInput?.hasBlock() || !isForkPostGloas(blockInput.forkName)) {
212
- // No block to pair this envelope with; drop silently
213
- continue;
214
- }
215
-
216
- const payloadInput = seenPayloadEnvelopeInputCache.get(blockInput.blockRootHex);
229
+ const envelopeBlockRootHex = toRootHex(envelope.message.beaconBlockRoot);
230
+ const payloadInput = seenPayloadEnvelopeInputCache.get(envelopeBlockRootHex);
217
231
  if (payloadInput === undefined) {
218
232
  // Unreachable given the loop above seeded an entry for every gloas block in the batch.
219
- continue;
233
+ // for the parent block, it's populated at BeaconChain init
234
+ throw new Error(`Missing PayloadEnvelopeInput for block ${envelopeBlockRootHex}`);
220
235
  }
221
236
 
222
237
  if (!payloadInput.hasPayloadEnvelope()) {
@@ -306,6 +321,8 @@ export async function downloadByRange({
306
321
  blobsRequest,
307
322
  columnsRequest,
308
323
  envelopesRequest,
324
+ parentPayloadRequest,
325
+ parentPayloadCommitments,
309
326
  peerDasMetrics,
310
327
  }: DownloadAndCacheByRangeProps): Promise<
311
328
  WarnResult<
@@ -322,6 +339,7 @@ export async function downloadByRange({
322
339
  blobsRequest,
323
340
  columnsRequest,
324
341
  envelopesRequest,
342
+ parentPayloadRequest,
325
343
  });
326
344
  } catch (err) {
327
345
  throw new DownloadByRangeError({
@@ -338,6 +356,8 @@ export async function downloadByRange({
338
356
  blobsRequest,
339
357
  columnsRequest,
340
358
  envelopesRequest,
359
+ parentPayloadRequest,
360
+ parentPayloadCommitments,
341
361
  peerDasMetrics,
342
362
  ...response,
343
363
  });
@@ -353,13 +373,14 @@ export async function requestByRange({
353
373
  blobsRequest,
354
374
  columnsRequest,
355
375
  envelopesRequest,
376
+ parentPayloadRequest,
356
377
  }: DownloadByRangeRequests & {
357
378
  network: INetwork;
358
379
  peerIdStr: PeerIdStr;
359
380
  }): Promise<DownloadByRangeResponses> {
360
381
  let blocks: undefined | SignedBeaconBlock[];
361
382
  let blobSidecars: undefined | deneb.BlobSidecars;
362
- let columnSidecars: undefined | DataColumnSidecar[];
383
+ const columnSidecars: DataColumnSidecar[] = [];
363
384
  const payloadEnvelopes: gloas.SignedExecutionPayloadEnvelope[] = [];
364
385
 
365
386
  const requests: Promise<unknown>[] = [];
@@ -368,17 +389,6 @@ export async function requestByRange({
368
389
  requests.push(
369
390
  network.sendBeaconBlocksByRange(peerIdStr, blocksRequest).then((blockResponse) => {
370
391
  blocks = blockResponse;
371
- const firstBlock = blockResponse.at(0);
372
- if (firstBlock && isGloasBeaconBlock(firstBlock.message)) {
373
- return network
374
- .sendExecutionPayloadEnvelopesByRoot(peerIdStr, [
375
- firstBlock.message.body.signedExecutionPayloadBid.message.parentBlockRoot,
376
- ])
377
- .then((envelopeResponse) => {
378
- payloadEnvelopes?.unshift(...envelopeResponse);
379
- });
380
- }
381
- return undefined;
382
392
  })
383
393
  );
384
394
  }
@@ -394,7 +404,7 @@ export async function requestByRange({
394
404
  if (columnsRequest) {
395
405
  requests.push(
396
406
  network.sendDataColumnSidecarsByRange(peerIdStr, columnsRequest).then((columnResponse) => {
397
- columnSidecars = columnResponse;
407
+ columnSidecars.push(...columnResponse);
398
408
  })
399
409
  );
400
410
  }
@@ -402,11 +412,34 @@ export async function requestByRange({
402
412
  if (envelopesRequest) {
403
413
  requests.push(
404
414
  network.sendExecutionPayloadEnvelopesByRange(peerIdStr, envelopesRequest).then((envelopeResponse) => {
405
- payloadEnvelopes?.push(...envelopeResponse);
415
+ payloadEnvelopes.push(...envelopeResponse);
406
416
  })
407
417
  );
408
418
  }
409
419
 
420
+ // Only happens on the 1st batch of a SyncChain — fetches whichever pieces of the
421
+ // dangling-parent's payload are still missing from the seen-cache entry.
422
+ if (parentPayloadRequest?.envelopeBlockRoot) {
423
+ requests.push(
424
+ network
425
+ .sendExecutionPayloadEnvelopesByRoot(peerIdStr, [parentPayloadRequest.envelopeBlockRoot])
426
+ .then((envelopeResponse) => {
427
+ payloadEnvelopes.push(...envelopeResponse);
428
+ })
429
+ );
430
+ }
431
+ if (parentPayloadRequest?.blockRoot && parentPayloadRequest.columns && parentPayloadRequest.columns.length > 0) {
432
+ requests.push(
433
+ network
434
+ .sendDataColumnSidecarsByRoot(peerIdStr, [
435
+ {blockRoot: parentPayloadRequest.blockRoot, columns: parentPayloadRequest.columns},
436
+ ])
437
+ .then((columnResponse) => {
438
+ columnSidecars.push(...columnResponse);
439
+ })
440
+ );
441
+ }
442
+
410
443
  await Promise.all(requests);
411
444
 
412
445
  return {
@@ -427,6 +460,8 @@ export async function validateResponses({
427
460
  blobsRequest,
428
461
  columnsRequest,
429
462
  envelopesRequest,
463
+ parentPayloadRequest,
464
+ parentPayloadCommitments,
430
465
  blocks,
431
466
  blobSidecars,
432
467
  columnSidecars,
@@ -436,6 +471,7 @@ export async function validateResponses({
436
471
  DownloadByRangeResponses & {
437
472
  config: ChainForkConfig;
438
473
  batchBlocks?: IBlockInput[];
474
+ parentPayloadCommitments?: ParentPayloadCommitments;
439
475
  peerDasMetrics?: BeaconMetrics["peerDas"] | null;
440
476
  }): Promise<
441
477
  WarnResult<
@@ -456,6 +492,11 @@ export async function validateResponses({
456
492
  );
457
493
  }
458
494
 
495
+ // `parentPayloadRequest` and `parentPayloadCommitments` must be supplied together
496
+ if ((parentPayloadRequest === undefined) !== (parentPayloadCommitments === undefined)) {
497
+ throw new Error("Coding error: parentPayloadRequest and parentPayloadCommitments must be both set or both unset");
498
+ }
499
+
459
500
  const validatedResponses: ValidatedResponses = {};
460
501
  let warnings: DownloadByRangeError[] | null = null;
461
502
 
@@ -467,21 +508,30 @@ export async function validateResponses({
467
508
  validatedResponses.validatedBlocks = result.result;
468
509
  }
469
510
 
511
+ const needsEnvelopeValidation = !!envelopesRequest || parentPayloadCommitments !== undefined;
470
512
  const dataRequest = blobsRequest ?? columnsRequest;
471
- if (!dataRequest && !envelopesRequest) {
513
+ if (!dataRequest && !needsEnvelopeValidation) {
472
514
  return {result: {responses: validatedResponses, payloadEnvelopes: null}, warnings};
473
515
  }
474
516
 
475
517
  if (!dataRequest) {
476
- // Only envelope validation needed
477
- let validatedPayloadEnvelopes: Map<Slot, gloas.SignedExecutionPayloadEnvelope> | null = null;
478
- if (envelopesRequest) {
479
- validatedPayloadEnvelopes = validateEnvelopesByRangeResponse(
480
- validatedResponses.validatedBlocks ?? [],
481
- batchBlocks,
482
- payloadEnvelopes ?? []
518
+ // Only envelope and/or parent-by-root validation needed
519
+ if (parentPayloadCommitments !== undefined) {
520
+ const parentValidated = await validateParentPayloadColumns(
521
+ parentPayloadCommitments,
522
+ columnSidecars ?? [],
523
+ peerDasMetrics
483
524
  );
525
+ if (parentValidated) {
526
+ validatedResponses.validatedColumnSidecars = [parentValidated];
527
+ }
484
528
  }
529
+ const validatedPayloadEnvelopes = validateEnvelopesByRangeResponse(
530
+ validatedResponses.validatedBlocks ?? [],
531
+ batchBlocks,
532
+ payloadEnvelopes ?? [],
533
+ parentPayloadCommitments
534
+ );
485
535
  return {result: {responses: validatedResponses, payloadEnvelopes: validatedPayloadEnvelopes}, warnings};
486
536
  }
487
537
 
@@ -540,19 +590,64 @@ export async function validateResponses({
540
590
  warnings = validatedColumnSidecarsResult.warnings;
541
591
  }
542
592
 
543
- // Validate envelopes if an envelopes request was made
593
+ // Parent columns (by-root): KZG-validate against parent's bid commitments and append.
594
+ if (parentPayloadCommitments !== undefined) {
595
+ const parentValidated = await validateParentPayloadColumns(
596
+ parentPayloadCommitments,
597
+ columnSidecars ?? [],
598
+ peerDasMetrics
599
+ );
600
+ if (parentValidated) {
601
+ validatedResponses.validatedColumnSidecars = [
602
+ ...(validatedResponses.validatedColumnSidecars ?? []),
603
+ parentValidated,
604
+ ];
605
+ }
606
+ }
607
+
544
608
  let validatedPayloadEnvelopes: Map<Slot, gloas.SignedExecutionPayloadEnvelope> | null = null;
545
- if (envelopesRequest) {
609
+ if (needsEnvelopeValidation) {
546
610
  validatedPayloadEnvelopes = validateEnvelopesByRangeResponse(
547
611
  validatedResponses.validatedBlocks ?? [],
548
612
  batchBlocks,
549
- payloadEnvelopes ?? []
613
+ payloadEnvelopes ?? [],
614
+ parentPayloadCommitments
550
615
  );
551
616
  }
552
617
 
553
618
  return {result: {responses: validatedResponses, payloadEnvelopes: validatedPayloadEnvelopes}, warnings};
554
619
  }
555
620
 
621
+ async function validateParentPayloadColumns(
622
+ parentPayloadCommitments: ParentPayloadCommitments,
623
+ columnSidecars: DataColumnSidecar[],
624
+ peerDasMetrics?: BeaconMetrics["peerDas"] | null
625
+ ): Promise<ValidatedColumnSidecars | null> {
626
+ const parentColumns: gloas.DataColumnSidecar[] = [];
627
+ for (const cs of columnSidecars) {
628
+ if (isGloasDataColumnSidecar(cs) && byteArrayEquals(cs.beaconBlockRoot, parentPayloadCommitments.blockRoot)) {
629
+ parentColumns.push(cs);
630
+ }
631
+ }
632
+
633
+ if (parentColumns.length === 0) {
634
+ return null;
635
+ }
636
+
637
+ parentColumns.sort((a, b) => a.index - b.index);
638
+ const parentSlot = parentColumns[0].slot;
639
+
640
+ await validateGloasBlockDataColumnSidecars(
641
+ parentSlot,
642
+ parentPayloadCommitments.blockRoot,
643
+ parentPayloadCommitments.kzgCommitments,
644
+ parentColumns,
645
+ peerDasMetrics
646
+ );
647
+
648
+ return {blockRoot: parentPayloadCommitments.blockRoot, columnSidecars: parentColumns};
649
+ }
650
+
556
651
  /**
557
652
  * Should not be called directly. Only exported for unit testing purposes
558
653
  *
@@ -1163,14 +1258,18 @@ export class DownloadByRangeError extends LodestarError<DownloadByRangeErrorType
1163
1258
 
1164
1259
  /**
1165
1260
  * Validates SignedExecutionPayloadEnvelopes received for a range request.
1166
- * For each envelope whose slot appears in the downloaded blocks, verifies that
1167
- * envelope.message.beaconBlockRoot matches the corresponding block's root.
1168
- * Envelopes for slots not in the batch (orphaned payloads) are silently ignored.
1261
+ *
1262
+ * Three categories of envelope slots:
1263
+ * - In-batch slot whose block we have: verify envelope.beaconBlockRoot matches.
1264
+ * - Dangling-parent envelope (only when `parentPayloadCommitments` is set, i.e. the first
1265
+ * batch of a `SyncChain`): keep if `envelope.beaconBlockRoot === parentPayloadCommitments.blockRoot`.
1266
+ * - Other "orphan" envelopes (e.g. unrelated slots): ignored.
1169
1267
  */
1170
1268
  export function validateEnvelopesByRangeResponse(
1171
1269
  validatedBlocks: ValidatedBlock[],
1172
1270
  batchBlocks: IBlockInput[] | undefined,
1173
- payloadEnvelopes: gloas.SignedExecutionPayloadEnvelope[]
1271
+ payloadEnvelopes: gloas.SignedExecutionPayloadEnvelope[],
1272
+ parentPayloadCommitments?: ParentPayloadCommitments
1174
1273
  ): Map<Slot, gloas.SignedExecutionPayloadEnvelope> {
1175
1274
  // Build a map of slot -> blockRoot for all blocks in the batch
1176
1275
  const batchBlockRoots = new Map<Slot, Uint8Array>();
@@ -1189,8 +1288,15 @@ export function validateEnvelopesByRangeResponse(
1189
1288
  const slot = payloadEnvelope.message.payload.slotNumber;
1190
1289
  const batchBlockRoot = batchBlockRoots.get(slot);
1191
1290
 
1192
- // Envelopes for slots not in the batch are silently ignored (orphaned payloads or a parent payload)
1193
1291
  if (batchBlockRoot === undefined) {
1292
+ // Keep the requested dangling-parent envelope only when its beaconBlockRoot matches
1293
+ // exactly. All other unrelated envelopes are dropped.
1294
+ if (
1295
+ parentPayloadCommitments !== undefined &&
1296
+ byteArrayEquals(payloadEnvelope.message.beaconBlockRoot, parentPayloadCommitments.blockRoot)
1297
+ ) {
1298
+ payloadEnvelopeMap.set(slot, payloadEnvelope);
1299
+ }
1194
1300
  continue;
1195
1301
  }
1196
1302