@lodestar/beacon-node 1.43.0-dev.b05ea98d04 → 1.43.0-dev.b0e1143d06

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 (132) 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/utils/chainSegment.d.ts +1 -3
  15. package/lib/chain/blocks/utils/chainSegment.d.ts.map +1 -1
  16. package/lib/chain/blocks/utils/chainSegment.js +29 -26
  17. package/lib/chain/blocks/utils/chainSegment.js.map +1 -1
  18. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.d.ts.map +1 -1
  19. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js +5 -4
  20. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js.map +1 -1
  21. package/lib/chain/chain.d.ts.map +1 -1
  22. package/lib/chain/chain.js +15 -1
  23. package/lib/chain/chain.js.map +1 -1
  24. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts +7 -6
  25. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts.map +1 -1
  26. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js +21 -5
  27. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js.map +1 -1
  28. package/lib/metrics/metrics/lodestar.d.ts +4 -0
  29. package/lib/metrics/metrics/lodestar.d.ts.map +1 -1
  30. package/lib/metrics/metrics/lodestar.js +5 -0
  31. package/lib/metrics/metrics/lodestar.js.map +1 -1
  32. package/lib/network/gossip/topic.d.ts +749 -2
  33. package/lib/network/gossip/topic.d.ts.map +1 -1
  34. package/lib/network/interface.d.ts +1 -0
  35. package/lib/network/interface.d.ts.map +1 -1
  36. package/lib/network/network.d.ts +1 -0
  37. package/lib/network/network.d.ts.map +1 -1
  38. package/lib/network/network.js +3 -0
  39. package/lib/network/network.js.map +1 -1
  40. package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
  41. package/lib/network/processor/gossipHandlers.js +31 -1
  42. package/lib/network/processor/gossipHandlers.js.map +1 -1
  43. package/lib/network/reqresp/ReqRespBeaconNode.d.ts.map +1 -1
  44. package/lib/network/reqresp/ReqRespBeaconNode.js +1 -1
  45. package/lib/network/reqresp/ReqRespBeaconNode.js.map +1 -1
  46. package/lib/network/reqresp/handlers/beaconBlocksByHead.d.ts +9 -0
  47. package/lib/network/reqresp/handlers/beaconBlocksByHead.d.ts.map +1 -0
  48. package/lib/network/reqresp/handlers/beaconBlocksByHead.js +61 -0
  49. package/lib/network/reqresp/handlers/beaconBlocksByHead.js.map +1 -0
  50. package/lib/network/reqresp/handlers/index.d.ts.map +1 -1
  51. package/lib/network/reqresp/handlers/index.js +5 -0
  52. package/lib/network/reqresp/handlers/index.js.map +1 -1
  53. package/lib/network/reqresp/interface.d.ts +1 -1
  54. package/lib/network/reqresp/interface.js +1 -1
  55. package/lib/network/reqresp/protocols.d.ts +1 -0
  56. package/lib/network/reqresp/protocols.d.ts.map +1 -1
  57. package/lib/network/reqresp/protocols.js +5 -0
  58. package/lib/network/reqresp/protocols.js.map +1 -1
  59. package/lib/network/reqresp/rateLimit.d.ts.map +1 -1
  60. package/lib/network/reqresp/rateLimit.js +4 -0
  61. package/lib/network/reqresp/rateLimit.js.map +1 -1
  62. package/lib/network/reqresp/score.d.ts.map +1 -1
  63. package/lib/network/reqresp/score.js +1 -0
  64. package/lib/network/reqresp/score.js.map +1 -1
  65. package/lib/network/reqresp/types.d.ts +3 -0
  66. package/lib/network/reqresp/types.d.ts.map +1 -1
  67. package/lib/network/reqresp/types.js +3 -0
  68. package/lib/network/reqresp/types.js.map +1 -1
  69. package/lib/sync/constants.d.ts +5 -1
  70. package/lib/sync/constants.d.ts.map +1 -1
  71. package/lib/sync/constants.js +5 -1
  72. package/lib/sync/constants.js.map +1 -1
  73. package/lib/sync/range/batch.d.ts +27 -6
  74. package/lib/sync/range/batch.d.ts.map +1 -1
  75. package/lib/sync/range/batch.js +149 -26
  76. package/lib/sync/range/batch.js.map +1 -1
  77. package/lib/sync/range/chain.d.ts +10 -2
  78. package/lib/sync/range/chain.d.ts.map +1 -1
  79. package/lib/sync/range/chain.js +55 -7
  80. package/lib/sync/range/chain.js.map +1 -1
  81. package/lib/sync/range/range.d.ts.map +1 -1
  82. package/lib/sync/range/range.js +54 -5
  83. package/lib/sync/range/range.js.map +1 -1
  84. package/lib/sync/range/utils/peerBalancer.d.ts +2 -1
  85. package/lib/sync/range/utils/peerBalancer.d.ts.map +1 -1
  86. package/lib/sync/range/utils/peerBalancer.js +8 -4
  87. package/lib/sync/range/utils/peerBalancer.js.map +1 -1
  88. package/lib/sync/sync.d.ts.map +1 -1
  89. package/lib/sync/sync.js +13 -0
  90. package/lib/sync/sync.js.map +1 -1
  91. package/lib/sync/unknownBlock.d.ts +7 -0
  92. package/lib/sync/unknownBlock.d.ts.map +1 -1
  93. package/lib/sync/unknownBlock.js +127 -12
  94. package/lib/sync/unknownBlock.js.map +1 -1
  95. package/lib/sync/utils/downloadByRange.d.ts +36 -9
  96. package/lib/sync/utils/downloadByRange.d.ts.map +1 -1
  97. package/lib/sync/utils/downloadByRange.js +87 -38
  98. package/lib/sync/utils/downloadByRange.js.map +1 -1
  99. package/lib/sync/utils/rateLimit.d.ts +2 -0
  100. package/lib/sync/utils/rateLimit.d.ts.map +1 -0
  101. package/lib/sync/utils/rateLimit.js +15 -0
  102. package/lib/sync/utils/rateLimit.js.map +1 -0
  103. package/package.json +16 -16
  104. package/src/chain/blocks/importExecutionPayload.ts +18 -6
  105. package/src/chain/blocks/index.ts +5 -7
  106. package/src/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.ts +27 -1
  107. package/src/chain/blocks/payloadEnvelopeInput/types.ts +17 -0
  108. package/src/chain/blocks/utils/chainSegment.ts +30 -27
  109. package/src/chain/blocks/verifyExecutionPayloadEnvelope.ts +7 -4
  110. package/src/chain/chain.ts +17 -0
  111. package/src/chain/seenCache/seenPayloadEnvelopeInput.ts +23 -6
  112. package/src/metrics/metrics/lodestar.ts +6 -0
  113. package/src/network/interface.ts +1 -0
  114. package/src/network/network.ts +12 -0
  115. package/src/network/processor/gossipHandlers.ts +35 -1
  116. package/src/network/reqresp/ReqRespBeaconNode.ts +1 -0
  117. package/src/network/reqresp/handlers/beaconBlocksByHead.ts +91 -0
  118. package/src/network/reqresp/handlers/index.ts +5 -0
  119. package/src/network/reqresp/interface.ts +1 -1
  120. package/src/network/reqresp/protocols.ts +6 -0
  121. package/src/network/reqresp/rateLimit.ts +4 -0
  122. package/src/network/reqresp/score.ts +1 -0
  123. package/src/network/reqresp/types.ts +5 -0
  124. package/src/sync/constants.ts +5 -1
  125. package/src/sync/range/batch.ts +188 -30
  126. package/src/sync/range/chain.ts +72 -13
  127. package/src/sync/range/range.ts +68 -5
  128. package/src/sync/range/utils/peerBalancer.ts +9 -3
  129. package/src/sync/sync.ts +13 -1
  130. package/src/sync/unknownBlock.ts +157 -12
  131. package/src/sync/utils/downloadByRange.ts +153 -39
  132. package/src/sync/utils/rateLimit.ts +16 -0
@@ -17,8 +17,6 @@ export type ChainSegmentResult = {warnings: OrphanedPayloadEnvelope[] | null};
17
17
  * Assert this chain segment of blocks is linear with slot numbers and hashes,
18
18
  * and that the provided envelopes are consistent with their respective blocks.
19
19
  *
20
- * Must be called after verifyBlocksSanityChecks so that parentBlock (from forkchoice)
21
- * is available to seed the execution hash chain.
22
20
  *
23
21
  * For each block:
24
22
  * - Verifies parent root + slot linearity
@@ -31,7 +29,7 @@ export function assertLinearChainSegment(
31
29
  config: ChainForkConfig,
32
30
  blocks: IBlockInput[],
33
31
  payloadEnvelopes: Map<Slot, PayloadEnvelopeInput> | null,
34
- parentBlock: ProtoBlock
32
+ parentBlock: ProtoBlock | null
35
33
  ): ChainSegmentResult {
36
34
  const warnings: OrphanedPayloadEnvelope[] = [];
37
35
 
@@ -39,15 +37,16 @@ export function assertLinearChainSegment(
39
37
  // Starts from the known forkchoice parent's execution hash.
40
38
  // - FULL variant (envelope present for slot): advances to envelope.payload.blockHash
41
39
  // - EMPTY variant (no envelope for slot): execution hash is unchanged
42
- // null only for pre-merge parents, which cannot precede gloas blocks.
43
- let currentExecHash: string | null = parentBlock.executionPayloadBlockHash;
40
+ let currentExecHash: string | null = parentBlock?.executionPayloadBlockHash ?? null;
44
41
  // Checkpoint sync first batch: parent is the anchor PENDING whose executionPayloadBlockHash
45
42
  // is the inherited parentBlockHash semantic (= grandparent's payload), not its own payload.
46
43
  // If parent's own payload envelope arrives in this batch, advance currentExecHash to that
47
44
  // payload's blockHash so the segment validation sees the true EL chain head.
48
- const parentPayloadInput = payloadEnvelopes?.get(parentBlock.slot);
49
- if (parentPayloadInput?.hasPayloadEnvelope()) {
50
- currentExecHash = parentPayloadInput.getBlockHashHex();
45
+ if (parentBlock !== null) {
46
+ const parentPayloadInput = payloadEnvelopes?.get(parentBlock.slot);
47
+ if (parentPayloadInput?.hasPayloadEnvelope()) {
48
+ currentExecHash = parentPayloadInput.getBlockHashHex();
49
+ }
51
50
  }
52
51
  // Track the execution hash before the last FULL advancement so we can recover
53
52
  // if the next block reveals that envelope was orphaned.
@@ -76,27 +75,31 @@ export function assertLinearChainSegment(
76
75
  }
77
76
  }
78
77
 
79
- if (isGloasBeaconBlock(block.message) && currentExecHash !== null) {
80
- // Verify the bid's parentBlockHash matches the tracked execution hash.
81
- // This ensures the block was built on the correct FULL or EMPTY variant of its parent.
82
- const bidParentHash = toRootHex(block.message.body.signedExecutionPayloadBid.message.parentBlockHash);
83
- if (bidParentHash !== currentExecHash) {
84
- // Maybe the previous slot's FULL envelope was orphaned try falling back.
85
- // If even prevExecHash doesn't match, the segment is non-linear.
86
- if (bidParentHash !== prevExecHash) {
87
- throw new BlockError(block, {
88
- code: BlockErrorCode.PARENT_PAYLOAD_UNKNOWN,
89
- parentRoot: toRootHex(block.message.parentRoot),
90
- parentBlockHash: bidParentHash,
91
- });
92
- }
93
- if (lastFullSlot !== null && payloadEnvelopes !== null) {
94
- const orphanedInput = payloadEnvelopes.get(lastFullSlot);
95
- if (orphanedInput != null) {
96
- warnings.push({slot: lastFullSlot, payloadEnvelopeInput: orphanedInput});
78
+ if (isGloasBeaconBlock(block.message)) {
79
+ // Bid check fires only when we have a seeded execution hash. With parentBlock=null the
80
+ // chain is seeded mid-segment by the first FULL envelope.
81
+ if (currentExecHash !== null) {
82
+ // Verify the bid's parentBlockHash matches the tracked execution hash.
83
+ // This ensures the block was built on the correct FULL or EMPTY variant of its parent.
84
+ const bidParentHash = toRootHex(block.message.body.signedExecutionPayloadBid.message.parentBlockHash);
85
+ if (bidParentHash !== currentExecHash) {
86
+ // Maybe the previous slot's FULL envelope was orphaned — try falling back.
87
+ // If even prevExecHash doesn't match, the segment is non-linear.
88
+ if (bidParentHash !== prevExecHash) {
89
+ throw new BlockError(block, {
90
+ code: BlockErrorCode.PARENT_PAYLOAD_UNKNOWN,
91
+ parentRoot: toRootHex(block.message.parentRoot),
92
+ parentBlockHash: bidParentHash,
93
+ });
94
+ }
95
+ if (lastFullSlot !== null && payloadEnvelopes !== null) {
96
+ const orphanedInput = payloadEnvelopes.get(lastFullSlot);
97
+ if (orphanedInput != null) {
98
+ warnings.push({slot: lastFullSlot, payloadEnvelopeInput: orphanedInput});
99
+ }
97
100
  }
101
+ currentExecHash = prevExecHash;
98
102
  }
99
- currentExecHash = prevExecHash;
100
103
  }
101
104
 
102
105
  const payloadInput = payloadEnvelopes?.get(slot) ?? null;
@@ -82,16 +82,19 @@ export function verifyExecutionPayloadEnvelope(
82
82
  }
83
83
  }
84
84
 
85
- // Verify the execution payload is valid
86
- if (payload.slotNumber !== state.slot) {
87
- throw new Error(`Slot mismatch between payload and state payload=${payload.slotNumber} state=${state.slot}`);
85
+ // should not use state.slot, it does not work for skipped slot checkpoint sync
86
+ const blockSlot = state.latestBlockHeader.slot;
87
+ if (payload.slotNumber !== blockSlot) {
88
+ throw new Error(
89
+ `Slot mismatch between payload and latest block header payload=${payload.slotNumber} latestBlockHeader=${blockSlot}`
90
+ );
88
91
  }
89
92
  if (!byteArrayEquals(payload.parentHash, state.latestBlockHash)) {
90
93
  throw new Error(
91
94
  `Parent hash mismatch between payload and state payload=${toRootHex(payload.parentHash)} state=${toRootHex(state.latestBlockHash)}`
92
95
  );
93
96
  }
94
- const expectedTimestamp = computeTimeAtSlot(config, state.slot, state.genesisTime);
97
+ const expectedTimestamp = computeTimeAtSlot(config, blockSlot, state.genesisTime);
95
98
  if (payload.timestamp !== expectedTimestamp) {
96
99
  throw new Error(
97
100
  `Timestamp mismatch between payload and state payload=${payload.timestamp} state=${expectedTimestamp}`
@@ -23,6 +23,7 @@ import {
23
23
  getEffectiveBalancesFromStateBytes,
24
24
  isStatePostAltair,
25
25
  isStatePostElectra,
26
+ isStatePostGloas,
26
27
  } from "@lodestar/state-transition";
27
28
  import {
28
29
  BeaconBlock,
@@ -390,6 +391,7 @@ export class BeaconChain implements IBeaconChain {
390
391
  metrics,
391
392
  logger
392
393
  );
394
+
393
395
  const regen = new QueuedStateRegenerator({
394
396
  config,
395
397
  forkChoice,
@@ -426,6 +428,21 @@ export class BeaconChain implements IBeaconChain {
426
428
  logger,
427
429
  });
428
430
 
431
+ const anchorBlockSlot = anchorState.latestBlockHeader.slot;
432
+ if (isStatePostGloas(anchorState) && anchorBlockSlot > 0) {
433
+ const anchorBid = anchorState.latestExecutionPayloadBid;
434
+ this.seenPayloadEnvelopeInputCache.addFromBid({
435
+ blockRootHex: toRootHex(checkpoint.root),
436
+ slot: anchorBlockSlot,
437
+ forkName: anchorState.forkName,
438
+ proposerIndex: anchorState.latestBlockHeader.proposerIndex,
439
+ bid: anchorBid,
440
+ sampledColumns: this.custodyConfig.sampledColumns,
441
+ custodyColumns: this.custodyConfig.custodyColumns,
442
+ timeCreatedSec: Math.floor(Date.now() / 1000),
443
+ });
444
+ }
445
+
429
446
  this.clock = clock;
430
447
  this.regen = regen;
431
448
  this.bls = bls;
@@ -7,7 +7,7 @@ import {Metrics} from "../../metrics/metrics.js";
7
7
  import {IClock} from "../../util/clock.js";
8
8
  import {SerializedCache} from "../../util/serializedCache.js";
9
9
  import {isDaOutOfRange} from "../blocks/blockInput/index.js";
10
- import {CreateFromBlockProps, PayloadEnvelopeInput} from "../blocks/payloadEnvelopeInput/index.js";
10
+ import {CreateFromBidProps, CreateFromBlockProps, PayloadEnvelopeInput} from "../blocks/payloadEnvelopeInput/index.js";
11
11
  import {ChainEvent, ChainEventEmitter} from "../emitter.js";
12
12
 
13
13
  export type {PayloadEnvelopeInputState} from "../blocks/payloadEnvelopeInput/index.js";
@@ -27,11 +27,7 @@ export type SeenPayloadEnvelopeInputModules = {
27
27
  /**
28
28
  * Cache for tracking PayloadEnvelopeInput instances, keyed by beacon block root.
29
29
  *
30
- * Created during block import when a block is processed. Two pruning paths:
31
- * - `prepareNextSlot` calls `pruneBelow(headParentSlot)` every slot once the head we'll build
32
- * on is known.
33
- * - `onFinalized` calls `pruneBelow(finalizedSlot)` on every finalization for bulk cleanup.
34
- *
30
+ * Created whenever we have a block because it needs block bid.
35
31
  * Steady state (linear chain, healthy progression): the cache holds ~2 entries — the head
36
32
  * (parent for next-slot production) and its parent (proposer-boost-reorg fallback). It can
37
33
  * transiently hold more during forks, range-sync bursts, or when `prepareNextSlot` skips
@@ -123,6 +119,27 @@ export class SeenPayloadEnvelopeInput {
123
119
  return input;
124
120
  }
125
121
 
122
+ /**
123
+ * Used at chain initialization to seed the anchor block's PayloadEnvelopeInput from
124
+ * `state.latestExecutionPayloadBid`.
125
+ */
126
+ addFromBid(props: Omit<CreateFromBidProps, "daOutOfRange">): PayloadEnvelopeInput {
127
+ const existing = this.payloadInputs.get(props.blockRootHex);
128
+ if (existing !== undefined) {
129
+ return existing;
130
+ }
131
+ const daOutOfRange = isDaOutOfRange(this.config, props.forkName, props.slot, this.clock.currentEpoch);
132
+ const input = PayloadEnvelopeInput.createFromBid({...props, daOutOfRange});
133
+ this.payloadInputs.set(props.blockRootHex, input);
134
+ this.metrics?.seenCache.payloadEnvelopeInput.created.inc();
135
+ this.logger?.verbose("SeenPayloadEnvelopeInput.addFromBid created new entry", {
136
+ slot: input.slot,
137
+ root: props.blockRootHex,
138
+ daOutOfRange,
139
+ });
140
+ return input;
141
+ }
142
+
126
143
  get(blockRootHex: RootHex): PayloadEnvelopeInput | undefined {
127
144
  return this.payloadInputs.get(blockRootHex);
128
145
  }
@@ -3,6 +3,7 @@ import {NotReorgedReason} from "@lodestar/fork-choice";
3
3
  import {ArchiveStoreTask} from "../../chain/archiveStore/archiveStore.js";
4
4
  import {FrequencyStateArchiveStep} from "../../chain/archiveStore/strategies/frequencyStateArchiveStrategy.js";
5
5
  import {BlockInputSource} from "../../chain/blocks/blockInput/index.js";
6
+ import {PayloadErrorCode} from "../../chain/blocks/importExecutionPayload.js";
6
7
  import {PayloadEnvelopeInputSource} from "../../chain/blocks/payloadEnvelopeInput/index.js";
7
8
  import {JobQueueItemType} from "../../chain/bls/index.js";
8
9
  import {AttestationErrorCode, BlockErrorCode} from "../../chain/errors/index.js";
@@ -890,6 +891,11 @@ export function createLodestarMetrics(
890
891
  labelNames: ["source"],
891
892
  buckets: [0.5, 1, 2, 4, 6, 12],
892
893
  }),
894
+ processPayloadErrors: register.gauge<{error: PayloadErrorCode | "NOT_PAYLOAD_ERROR"}>({
895
+ name: "lodestar_gossip_execution_payload_envelope_process_payload_errors",
896
+ help: "Count of errors, by error type, while processing execution payload envelopes",
897
+ labelNames: ["error"],
898
+ }),
893
899
  },
894
900
  // recovery in the case of specific blob rows required
895
901
  recoverBlobSidecars: {
@@ -78,6 +78,7 @@ export interface INetwork extends INetworkCorePublic {
78
78
  // ReqResp
79
79
  sendBeaconBlocksByRange(peerId: PeerIdStr, request: phase0.BeaconBlocksByRangeRequest): Promise<SignedBeaconBlock[]>;
80
80
  sendBeaconBlocksByRoot(peerId: PeerIdStr, request: BeaconBlocksByRootRequest): Promise<SignedBeaconBlock[]>;
81
+ sendBeaconBlocksByHead(peerId: PeerIdStr, request: fulu.BeaconBlocksByHeadRequest): Promise<SignedBeaconBlock[]>;
81
82
  sendBlobSidecarsByRange(peerId: PeerIdStr, request: deneb.BlobSidecarsByRangeRequest): Promise<deneb.BlobSidecar[]>;
82
83
  sendBlobSidecarsByRoot(peerId: PeerIdStr, request: BlobSidecarsByRootRequest): Promise<deneb.BlobSidecar[]>;
83
84
  sendDataColumnSidecarsByRange(
@@ -578,6 +578,18 @@ export class Network implements INetwork {
578
578
  );
579
579
  }
580
580
 
581
+ async sendBeaconBlocksByHead(
582
+ peerId: PeerIdStr,
583
+ request: fulu.BeaconBlocksByHeadRequest
584
+ ): Promise<SignedBeaconBlock[]> {
585
+ return collectMaxResponseTypedWithBytes(
586
+ this.sendReqRespRequest(peerId, ReqRespMethod.BeaconBlocksByHead, [Version.V1], request),
587
+ Math.min(request.count, this.config.MAX_REQUEST_BLOCKS_DENEB),
588
+ responseSszTypeByMethod[ReqRespMethod.BeaconBlocksByHead],
589
+ this.chain.serializedCache
590
+ );
591
+ }
592
+
581
593
  async sendLightClientBootstrap(peerId: PeerIdStr, request: Root): Promise<LightClientBootstrap> {
582
594
  return collectExactOneTyped(
583
595
  this.sendReqRespRequest(peerId, ReqRespMethod.LightClientBootstrap, [Version.V1], request),
@@ -35,6 +35,7 @@ import {
35
35
  IBlockInput,
36
36
  isBlockInputColumns,
37
37
  } from "../../chain/blocks/blockInput/index.js";
38
+ import {PayloadError, PayloadErrorCode} from "../../chain/blocks/importExecutionPayload.js";
38
39
  import {PayloadEnvelopeInput, PayloadEnvelopeInputSource} from "../../chain/blocks/payloadEnvelopeInput/index.js";
39
40
  import {BlobSidecarValidation} from "../../chain/blocks/types.js";
40
41
  import {ChainEvent} from "../../chain/emitter.js";
@@ -1148,7 +1149,40 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
1148
1149
  });
1149
1150
 
1150
1151
  chain.processExecutionPayload(payloadInput, {validSignature: true}).catch((e) => {
1151
- chain.logger.debug("Error processing execution payload from gossip", {slot, root: blockRootHex}, e as Error);
1152
+ // Adjust verbosity based on error type
1153
+ let logLevel: LogLevel;
1154
+
1155
+ if (e instanceof PayloadError) {
1156
+ switch (e.type.code) {
1157
+ // BLOCK_NOT_IN_FORK_CHOICE should not happen, validateGossipExecutionPayloadEnvelope above
1158
+ // already verified the block is in fork choice
1159
+ case PayloadErrorCode.BLOCK_NOT_IN_FORK_CHOICE:
1160
+ case PayloadErrorCode.MISS_BLOCK_STATE:
1161
+ case PayloadErrorCode.EXECUTION_ENGINE_ERROR:
1162
+ // Errors might indicate an issue with our node or the connected EL client
1163
+ logLevel = LogLevel.error;
1164
+ break;
1165
+ // INVALID_SIGNATURE should not happen, signature is verified during gossip validation
1166
+ case PayloadErrorCode.INVALID_SIGNATURE:
1167
+ case PayloadErrorCode.ENVELOPE_VERIFICATION_ERROR:
1168
+ case PayloadErrorCode.EXECUTION_ENGINE_INVALID:
1169
+ core.reportPeer(peerIdStr, PeerAction.LowToleranceError, "BadGossipPayload");
1170
+ // Misbehaving peer, but could highlight an issue in another client
1171
+ logLevel = LogLevel.warn;
1172
+ break;
1173
+ }
1174
+ } else {
1175
+ // Any unexpected error
1176
+ logLevel = LogLevel.error;
1177
+ }
1178
+ metrics?.gossipExecutionPayloadEnvelope.processPayloadErrors.inc({
1179
+ error: e instanceof PayloadError ? e.type.code : "NOT_PAYLOAD_ERROR",
1180
+ });
1181
+ chain.logger[logLevel](
1182
+ "Error processing execution payload from gossip",
1183
+ {slot, peer: peerIdStr, root: blockRootHex},
1184
+ e as Error
1185
+ );
1152
1186
  });
1153
1187
  },
1154
1188
  [GossipType.payload_attestation_message]: async ({
@@ -286,6 +286,7 @@ export class ReqRespBeaconNode extends ReqResp {
286
286
  // instead of protocol version. This is not easily fixable with our current architecture.
287
287
  // See https://github.com/ChainSafe/lodestar/pull/8168 for more details.
288
288
  [protocols.StatusV2(fork, this.config), this.onStatus.bind(this)],
289
+ [protocols.BeaconBlocksByHead(fork, this.config), this.getHandler(ReqRespMethod.BeaconBlocksByHead)],
289
290
  [
290
291
  protocols.DataColumnSidecarsByRoot(fork, this.config),
291
292
  this.getHandler(ReqRespMethod.DataColumnSidecarsByRoot),
@@ -0,0 +1,91 @@
1
+ import {PeerId} from "@libp2p/interface";
2
+ import {BeaconConfig} from "@lodestar/config";
3
+ import {ForkName, GENESIS_EPOCH, GENESIS_SLOT, isForkPostDeneb} from "@lodestar/params";
4
+ import {RespStatus, ResponseError, ResponseOutgoing} from "@lodestar/reqresp";
5
+ import {computeEpochAtSlot, computeStartSlotAtEpoch} from "@lodestar/state-transition";
6
+ import {fulu} from "@lodestar/types";
7
+ import {toRootHex} from "@lodestar/utils";
8
+ import {IBeaconChain} from "../../../chain/index.js";
9
+ import {getParentRootFromSignedBeaconBlockSerialized} from "../../../util/sszBytes.js";
10
+ import {prettyPrintPeerId} from "../../util.js";
11
+
12
+ // See https://github.com/ethereum/consensus-specs/pull/5181
13
+ export async function* onBeaconBlocksByHead(
14
+ request: fulu.BeaconBlocksByHeadRequest,
15
+ chain: IBeaconChain,
16
+ peerId: PeerId,
17
+ peerClient: string
18
+ ): AsyncIterable<ResponseOutgoing> {
19
+ const currentFork = chain.config.getForkName(chain.clock.currentSlot);
20
+ const {beaconRoot, count} = validateBeaconBlocksByHeadRequest(currentFork, chain.config, request);
21
+
22
+ const requestedRootHex = toRootHex(beaconRoot);
23
+ let blockRootHex = requestedRootHex;
24
+ const minimumRequestEpoch = Math.max(
25
+ GENESIS_EPOCH,
26
+ chain.clock.currentEpoch - chain.config.MIN_EPOCHS_FOR_BLOCK_REQUESTS
27
+ );
28
+ const minimumRequestSlot = computeStartSlotAtEpoch(minimumRequestEpoch);
29
+
30
+ for (let blocksSent = 0; blocksSent < count; blocksSent++) {
31
+ const blockBytes = await chain.getSerializedBlockByRoot(blockRootHex);
32
+ if (!blockBytes) {
33
+ if (blocksSent === 0) {
34
+ throw new ResponseError(RespStatus.RESOURCE_UNAVAILABLE, `Unknown block root ${requestedRootHex}`);
35
+ }
36
+ return;
37
+ }
38
+
39
+ if (blockBytes.slot < minimumRequestSlot) {
40
+ if (blocksSent === 0) {
41
+ chain.logger.verbose("Peer requested unavailable block for BeaconBlocksByHead", {
42
+ peer: prettyPrintPeerId(peerId),
43
+ client: peerClient,
44
+ requestedRoot: requestedRootHex,
45
+ slot: blockBytes.slot,
46
+ minimumRequestSlot,
47
+ });
48
+ }
49
+ return;
50
+ }
51
+
52
+ yield {
53
+ data: blockBytes.block,
54
+ boundary: chain.config.getForkBoundaryAtEpoch(computeEpochAtSlot(blockBytes.slot)),
55
+ };
56
+
57
+ if (blockBytes.slot === GENESIS_SLOT) {
58
+ return;
59
+ }
60
+
61
+ const parentRootHex = getParentRootFromSignedBeaconBlockSerialized(blockBytes.block);
62
+ if (parentRootHex === null) {
63
+ throw new ResponseError(
64
+ RespStatus.SERVER_ERROR,
65
+ `Invalid block bytes for root ${blockRootHex} slot ${blockBytes.slot}`
66
+ );
67
+ }
68
+ blockRootHex = parentRootHex;
69
+ }
70
+ }
71
+
72
+ export function validateBeaconBlocksByHeadRequest(
73
+ fork: ForkName,
74
+ config: BeaconConfig,
75
+ request: fulu.BeaconBlocksByHeadRequest
76
+ ): fulu.BeaconBlocksByHeadRequest {
77
+ const {beaconRoot} = request;
78
+ let {count} = request;
79
+
80
+ if (count < 1) {
81
+ throw new ResponseError(RespStatus.INVALID_REQUEST, "count < 1");
82
+ }
83
+
84
+ const maxRequestBlocks = isForkPostDeneb(fork) ? config.MAX_REQUEST_BLOCKS_DENEB : config.MAX_REQUEST_BLOCKS;
85
+
86
+ if (count > maxRequestBlocks) {
87
+ count = maxRequestBlocks;
88
+ }
89
+
90
+ return {beaconRoot, count};
91
+ }
@@ -9,6 +9,7 @@ import {
9
9
  ExecutionPayloadEnvelopesByRootRequestType,
10
10
  } from "../../../util/types.js";
11
11
  import {GetReqRespHandlerFn, ReqRespMethod} from "../types.js";
12
+ import {onBeaconBlocksByHead} from "./beaconBlocksByHead.js";
12
13
  import {onBeaconBlocksByRange} from "./beaconBlocksByRange.js";
13
14
  import {onBeaconBlocksByRoot} from "./beaconBlocksByRoot.js";
14
15
  import {onBlobSidecarsByRange} from "./blobSidecarsByRange.js";
@@ -47,6 +48,10 @@ export function getReqRespHandlers({db, chain}: {db: IBeaconDb; chain: IBeaconCh
47
48
  const body = BeaconBlocksByRootRequestType(fork, chain.config).deserialize(req.data);
48
49
  return onBeaconBlocksByRoot(body, chain);
49
50
  },
51
+ [ReqRespMethod.BeaconBlocksByHead]: (req, peerId, peerClient) => {
52
+ const body = ssz.fulu.BeaconBlocksByHeadRequest.deserialize(req.data);
53
+ return onBeaconBlocksByHead(body, chain, peerId, peerClient);
54
+ },
50
55
  [ReqRespMethod.BlobSidecarsByRoot]: (req) => {
51
56
  const fork = chain.config.getForkName(chain.clock.currentSlot);
52
57
  const body = BlobSidecarsByRootRequestType(fork, chain.config).deserialize(req.data);
@@ -33,7 +33,7 @@ export enum RespStatus {
33
33
  */
34
34
  SERVER_ERROR = 2,
35
35
  /**
36
- * The responder does not have requested resource. The response payload adheres to the ErrorMessage schema (described below). Note: This response code is only valid as a response to BlocksByRange
36
+ * The responder does not have requested resource. The response payload adheres to the ErrorMessage schema.
37
37
  */
38
38
  RESOURCE_UNAVAILABLE = 3,
39
39
  /**
@@ -70,6 +70,12 @@ export const BeaconBlocksByRootV2 = toProtocol({
70
70
  contextBytesType: ContextBytesType.ForkDigest,
71
71
  });
72
72
 
73
+ export const BeaconBlocksByHead = toProtocol({
74
+ method: ReqRespMethod.BeaconBlocksByHead,
75
+ version: Version.V1,
76
+ contextBytesType: ContextBytesType.ForkDigest,
77
+ });
78
+
73
79
  export const BlobSidecarsByRange = toProtocol({
74
80
  method: ReqRespMethod.BlobSidecarsByRange,
75
81
  version: Version.V1,
@@ -40,6 +40,10 @@ export const rateLimitQuotas: (fork: ForkName, config: BeaconConfig) => Record<R
40
40
  },
41
41
  getRequestCount: getRequestCountFn(fork, config, ReqRespMethod.BeaconBlocksByRoot, (req) => req.length),
42
42
  },
43
+ [ReqRespMethod.BeaconBlocksByHead]: {
44
+ byPeer: {quota: config.MAX_REQUEST_BLOCKS_DENEB, quotaTimeMs: 10_000},
45
+ getRequestCount: getRequestCountFn(fork, config, ReqRespMethod.BeaconBlocksByHead, (req) => req.count),
46
+ },
43
47
  [ReqRespMethod.BlobSidecarsByRange]: {
44
48
  // Rationale: MAX_REQUEST_BLOCKS_DENEB * MAX_BLOBS_PER_BLOCK
45
49
  byPeer: {
@@ -46,6 +46,7 @@ export function onOutgoingReqRespError(e: RequestError, method: ReqRespMethod):
46
46
  return PeerAction.LowToleranceError;
47
47
  case ReqRespMethod.BeaconBlocksByRange:
48
48
  case ReqRespMethod.BeaconBlocksByRoot:
49
+ case ReqRespMethod.BeaconBlocksByHead:
49
50
  case ReqRespMethod.ExecutionPayloadEnvelopesByRoot:
50
51
  case ReqRespMethod.ExecutionPayloadEnvelopesByRange:
51
52
  return PeerAction.MidToleranceError;
@@ -42,6 +42,7 @@ export enum ReqRespMethod {
42
42
  Metadata = "metadata",
43
43
  BeaconBlocksByRange = "beacon_blocks_by_range",
44
44
  BeaconBlocksByRoot = "beacon_blocks_by_root",
45
+ BeaconBlocksByHead = "beacon_blocks_by_head",
45
46
  BlobSidecarsByRange = "blob_sidecars_by_range",
46
47
  BlobSidecarsByRoot = "blob_sidecars_by_root",
47
48
  DataColumnSidecarsByRange = "data_column_sidecars_by_range",
@@ -62,6 +63,7 @@ export type RequestBodyByMethod = {
62
63
  [ReqRespMethod.Metadata]: null;
63
64
  [ReqRespMethod.BeaconBlocksByRange]: phase0.BeaconBlocksByRangeRequest;
64
65
  [ReqRespMethod.BeaconBlocksByRoot]: BeaconBlocksByRootRequest;
66
+ [ReqRespMethod.BeaconBlocksByHead]: fulu.BeaconBlocksByHeadRequest;
65
67
  [ReqRespMethod.BlobSidecarsByRange]: deneb.BlobSidecarsByRangeRequest;
66
68
  [ReqRespMethod.BlobSidecarsByRoot]: BlobSidecarsByRootRequest;
67
69
  [ReqRespMethod.DataColumnSidecarsByRange]: fulu.DataColumnSidecarsByRangeRequest;
@@ -82,6 +84,7 @@ type ResponseBodyByMethod = {
82
84
  // Do not matter
83
85
  [ReqRespMethod.BeaconBlocksByRange]: SignedBeaconBlock;
84
86
  [ReqRespMethod.BeaconBlocksByRoot]: SignedBeaconBlock;
87
+ [ReqRespMethod.BeaconBlocksByHead]: SignedBeaconBlock;
85
88
  [ReqRespMethod.BlobSidecarsByRange]: deneb.BlobSidecar;
86
89
  [ReqRespMethod.BlobSidecarsByRoot]: deneb.BlobSidecar;
87
90
  [ReqRespMethod.DataColumnSidecarsByRange]: DataColumnSidecar;
@@ -111,6 +114,7 @@ export const requestSszTypeByMethod: (
111
114
 
112
115
  [ReqRespMethod.BeaconBlocksByRange]: ssz.phase0.BeaconBlocksByRangeRequest,
113
116
  [ReqRespMethod.BeaconBlocksByRoot]: BeaconBlocksByRootRequestType(fork, config),
117
+ [ReqRespMethod.BeaconBlocksByHead]: ssz.fulu.BeaconBlocksByHeadRequest,
114
118
  [ReqRespMethod.BlobSidecarsByRange]: ssz.deneb.BlobSidecarsByRangeRequest,
115
119
  [ReqRespMethod.BlobSidecarsByRoot]: BlobSidecarsByRootRequestType(fork, config),
116
120
  [ReqRespMethod.DataColumnSidecarsByRange]: ssz.fulu.DataColumnSidecarsByRangeRequest,
@@ -142,6 +146,7 @@ export const responseSszTypeByMethod: {[K in ReqRespMethod]: ResponseTypeGetter<
142
146
  version === Version.V1 ? ssz.phase0.Metadata : version === Version.V2 ? ssz.altair.Metadata : ssz.fulu.Metadata,
143
147
  [ReqRespMethod.BeaconBlocksByRange]: blocksResponseType,
144
148
  [ReqRespMethod.BeaconBlocksByRoot]: blocksResponseType,
149
+ [ReqRespMethod.BeaconBlocksByHead]: (fork) => ssz[fork].SignedBeaconBlock,
145
150
  [ReqRespMethod.BlobSidecarsByRange]: () => ssz.deneb.BlobSidecar,
146
151
  [ReqRespMethod.BlobSidecarsByRoot]: () => ssz.deneb.BlobSidecar,
147
152
  [ReqRespMethod.LightClientBootstrap]: (fork) => sszTypesFor(onlyPostAltairFork(fork)).LightClientBootstrap,
@@ -7,7 +7,11 @@ export const MIN_FINALIZED_CHAIN_VALIDATED_EPOCHS = 10;
7
7
  /** The number of times to retry a batch before it is considered failed. */
8
8
  export const MAX_BATCH_DOWNLOAD_ATTEMPTS = 5;
9
9
 
10
- /** Backoff before assigning more range-sync batches to a peer that rate-limited us. */
10
+ /**
11
+ * Backoff before assigning more range-sync batches to a peer that rate-limited us.
12
+ *
13
+ * Note: this is used when rate limited due to MAX_CONCURRENT_REQUESTS
14
+ */
11
15
  export const RATE_LIMITED_PEER_BACKOFF_MS = 5_000;
12
16
 
13
17
  /**