@lodestar/beacon-node 1.43.0-dev.ca1fc40294 → 1.43.0-dev.dfb984e779

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 (170) hide show
  1. package/lib/api/impl/beacon/blocks/index.d.ts.map +1 -1
  2. package/lib/api/impl/beacon/blocks/index.js +3 -2
  3. package/lib/api/impl/beacon/blocks/index.js.map +1 -1
  4. package/lib/api/impl/lodestar/index.js +1 -1
  5. package/lib/api/impl/lodestar/index.js.map +1 -1
  6. package/lib/chain/blocks/importBlock.d.ts.map +1 -1
  7. package/lib/chain/blocks/importBlock.js +6 -3
  8. package/lib/chain/blocks/importBlock.js.map +1 -1
  9. package/lib/chain/blocks/importExecutionPayload.d.ts +26 -14
  10. package/lib/chain/blocks/importExecutionPayload.d.ts.map +1 -1
  11. package/lib/chain/blocks/importExecutionPayload.js +73 -77
  12. package/lib/chain/blocks/importExecutionPayload.js.map +1 -1
  13. package/lib/chain/blocks/index.d.ts +5 -3
  14. package/lib/chain/blocks/index.d.ts.map +1 -1
  15. package/lib/chain/blocks/index.js +28 -10
  16. package/lib/chain/blocks/index.js.map +1 -1
  17. package/lib/chain/blocks/payloadEnvelopeProcessor.js +2 -2
  18. package/lib/chain/blocks/payloadEnvelopeProcessor.js.map +1 -1
  19. package/lib/chain/blocks/types.d.ts +14 -20
  20. package/lib/chain/blocks/types.d.ts.map +1 -1
  21. package/lib/chain/blocks/utils/chainSegment.d.ts +23 -2
  22. package/lib/chain/blocks/utils/chainSegment.d.ts.map +1 -1
  23. package/lib/chain/blocks/utils/chainSegment.js +81 -12
  24. package/lib/chain/blocks/utils/chainSegment.js.map +1 -1
  25. package/lib/chain/blocks/verifyBlock.d.ts +3 -2
  26. package/lib/chain/blocks/verifyBlock.d.ts.map +1 -1
  27. package/lib/chain/blocks/verifyBlock.js +30 -5
  28. package/lib/chain/blocks/verifyBlock.js.map +1 -1
  29. package/lib/chain/blocks/verifyBlocksSanityChecks.d.ts.map +1 -1
  30. package/lib/chain/blocks/verifyBlocksSanityChecks.js +15 -4
  31. package/lib/chain/blocks/verifyBlocksSanityChecks.js.map +1 -1
  32. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.d.ts +24 -0
  33. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.d.ts.map +1 -0
  34. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js +76 -0
  35. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js.map +1 -0
  36. package/lib/chain/blocks/writePayloadEnvelopeInputToDb.d.ts +1 -1
  37. package/lib/chain/blocks/writePayloadEnvelopeInputToDb.d.ts.map +1 -1
  38. package/lib/chain/blocks/writePayloadEnvelopeInputToDb.js +2 -11
  39. package/lib/chain/blocks/writePayloadEnvelopeInputToDb.js.map +1 -1
  40. package/lib/chain/chain.d.ts +3 -2
  41. package/lib/chain/chain.d.ts.map +1 -1
  42. package/lib/chain/chain.js +14 -3
  43. package/lib/chain/chain.js.map +1 -1
  44. package/lib/chain/errors/blockError.d.ts +8 -1
  45. package/lib/chain/errors/blockError.d.ts.map +1 -1
  46. package/lib/chain/errors/blockError.js +2 -0
  47. package/lib/chain/errors/blockError.js.map +1 -1
  48. package/lib/chain/errors/executionPayloadBid.d.ts +5 -0
  49. package/lib/chain/errors/executionPayloadBid.d.ts.map +1 -1
  50. package/lib/chain/errors/executionPayloadBid.js +1 -0
  51. package/lib/chain/errors/executionPayloadBid.js.map +1 -1
  52. package/lib/chain/errors/executionPayloadEnvelope.d.ts +5 -0
  53. package/lib/chain/errors/executionPayloadEnvelope.d.ts.map +1 -1
  54. package/lib/chain/errors/executionPayloadEnvelope.js +1 -0
  55. package/lib/chain/errors/executionPayloadEnvelope.js.map +1 -1
  56. package/lib/chain/forkChoice/index.js +2 -2
  57. package/lib/chain/forkChoice/index.js.map +1 -1
  58. package/lib/chain/interface.d.ts +3 -2
  59. package/lib/chain/interface.d.ts.map +1 -1
  60. package/lib/chain/interface.js.map +1 -1
  61. package/lib/chain/prepareNextSlot.d.ts.map +1 -1
  62. package/lib/chain/prepareNextSlot.js +30 -10
  63. package/lib/chain/prepareNextSlot.js.map +1 -1
  64. package/lib/chain/produceBlock/produceBlockBody.d.ts +3 -2
  65. package/lib/chain/produceBlock/produceBlockBody.d.ts.map +1 -1
  66. package/lib/chain/produceBlock/produceBlockBody.js +34 -13
  67. package/lib/chain/produceBlock/produceBlockBody.js.map +1 -1
  68. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts +11 -4
  69. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts.map +1 -1
  70. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js +20 -18
  71. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js.map +1 -1
  72. package/lib/chain/validation/block.d.ts.map +1 -1
  73. package/lib/chain/validation/block.js +1 -0
  74. package/lib/chain/validation/block.js.map +1 -1
  75. package/lib/chain/validation/executionPayloadBid.d.ts.map +1 -1
  76. package/lib/chain/validation/executionPayloadBid.js +13 -1
  77. package/lib/chain/validation/executionPayloadBid.js.map +1 -1
  78. package/lib/chain/validation/executionPayloadEnvelope.d.ts.map +1 -1
  79. package/lib/chain/validation/executionPayloadEnvelope.js +11 -1
  80. package/lib/chain/validation/executionPayloadEnvelope.js.map +1 -1
  81. package/lib/metrics/metrics/lodestar.d.ts +1 -0
  82. package/lib/metrics/metrics/lodestar.d.ts.map +1 -1
  83. package/lib/metrics/metrics/lodestar.js +4 -0
  84. package/lib/metrics/metrics/lodestar.js.map +1 -1
  85. package/lib/network/processor/gossipHandlers.js +4 -6
  86. package/lib/network/processor/gossipHandlers.js.map +1 -1
  87. package/lib/network/reqresp/handlers/beaconBlocksByRange.d.ts.map +1 -1
  88. package/lib/network/reqresp/handlers/beaconBlocksByRange.js +14 -6
  89. package/lib/network/reqresp/handlers/beaconBlocksByRange.js.map +1 -1
  90. package/lib/network/reqresp/handlers/blobSidecarsByRange.d.ts.map +1 -1
  91. package/lib/network/reqresp/handlers/blobSidecarsByRange.js +11 -5
  92. package/lib/network/reqresp/handlers/blobSidecarsByRange.js.map +1 -1
  93. package/lib/network/reqresp/handlers/dataColumnSidecarsByRange.d.ts.map +1 -1
  94. package/lib/network/reqresp/handlers/dataColumnSidecarsByRange.js +17 -5
  95. package/lib/network/reqresp/handlers/dataColumnSidecarsByRange.js.map +1 -1
  96. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRange.d.ts.map +1 -1
  97. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRange.js +7 -4
  98. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRange.js.map +1 -1
  99. package/lib/node/notifier.js +7 -1
  100. package/lib/node/notifier.js.map +1 -1
  101. package/lib/sync/range/batch.d.ts +12 -2
  102. package/lib/sync/range/batch.d.ts.map +1 -1
  103. package/lib/sync/range/batch.js +56 -30
  104. package/lib/sync/range/batch.js.map +1 -1
  105. package/lib/sync/range/chain.d.ts +6 -2
  106. package/lib/sync/range/chain.d.ts.map +1 -1
  107. package/lib/sync/range/chain.js +4 -3
  108. package/lib/sync/range/chain.js.map +1 -1
  109. package/lib/sync/range/range.d.ts.map +1 -1
  110. package/lib/sync/range/range.js +17 -6
  111. package/lib/sync/range/range.js.map +1 -1
  112. package/lib/sync/types.d.ts +34 -0
  113. package/lib/sync/types.d.ts.map +1 -1
  114. package/lib/sync/types.js +34 -0
  115. package/lib/sync/types.js.map +1 -1
  116. package/lib/sync/unknownBlock.d.ts +24 -1
  117. package/lib/sync/unknownBlock.d.ts.map +1 -1
  118. package/lib/sync/unknownBlock.js +649 -53
  119. package/lib/sync/unknownBlock.js.map +1 -1
  120. package/lib/sync/utils/downloadByRange.d.ts +46 -10
  121. package/lib/sync/utils/downloadByRange.d.ts.map +1 -1
  122. package/lib/sync/utils/downloadByRange.js +147 -24
  123. package/lib/sync/utils/downloadByRange.js.map +1 -1
  124. package/lib/sync/utils/downloadByRoot.d.ts.map +1 -1
  125. package/lib/sync/utils/downloadByRoot.js +6 -2
  126. package/lib/sync/utils/downloadByRoot.js.map +1 -1
  127. package/lib/sync/utils/pendingBlocksTree.d.ts +0 -1
  128. package/lib/sync/utils/pendingBlocksTree.d.ts.map +1 -1
  129. package/lib/sync/utils/pendingBlocksTree.js +0 -9
  130. package/lib/sync/utils/pendingBlocksTree.js.map +1 -1
  131. package/package.json +16 -15
  132. package/src/api/impl/beacon/blocks/index.ts +5 -2
  133. package/src/api/impl/lodestar/index.ts +1 -1
  134. package/src/chain/blocks/importBlock.ts +4 -2
  135. package/src/chain/blocks/importExecutionPayload.ts +92 -97
  136. package/src/chain/blocks/index.ts +44 -13
  137. package/src/chain/blocks/payloadEnvelopeProcessor.ts +2 -2
  138. package/src/chain/blocks/types.ts +14 -25
  139. package/src/chain/blocks/utils/chainSegment.ts +106 -17
  140. package/src/chain/blocks/verifyBlock.ts +35 -6
  141. package/src/chain/blocks/verifyBlocksSanityChecks.ts +16 -7
  142. package/src/chain/blocks/verifyExecutionPayloadEnvelope.ts +129 -0
  143. package/src/chain/blocks/writePayloadEnvelopeInputToDb.ts +9 -18
  144. package/src/chain/chain.ts +23 -3
  145. package/src/chain/errors/blockError.ts +4 -1
  146. package/src/chain/errors/executionPayloadBid.ts +6 -0
  147. package/src/chain/errors/executionPayloadEnvelope.ts +6 -0
  148. package/src/chain/forkChoice/index.ts +2 -2
  149. package/src/chain/interface.ts +7 -1
  150. package/src/chain/prepareNextSlot.ts +42 -12
  151. package/src/chain/produceBlock/produceBlockBody.ts +37 -11
  152. package/src/chain/seenCache/seenPayloadEnvelopeInput.ts +22 -20
  153. package/src/chain/validation/block.ts +1 -0
  154. package/src/chain/validation/executionPayloadBid.ts +14 -0
  155. package/src/chain/validation/executionPayloadEnvelope.ts +12 -2
  156. package/src/metrics/metrics/lodestar.ts +4 -0
  157. package/src/network/processor/gossipHandlers.ts +6 -6
  158. package/src/network/reqresp/handlers/beaconBlocksByRange.ts +14 -6
  159. package/src/network/reqresp/handlers/blobSidecarsByRange.ts +11 -5
  160. package/src/network/reqresp/handlers/dataColumnSidecarsByRange.ts +17 -5
  161. package/src/network/reqresp/handlers/executionPayloadEnvelopesByRange.ts +7 -4
  162. package/src/node/notifier.ts +8 -1
  163. package/src/sync/range/batch.ts +90 -35
  164. package/src/sync/range/chain.ts +13 -5
  165. package/src/sync/range/range.ts +18 -6
  166. package/src/sync/types.ts +72 -0
  167. package/src/sync/unknownBlock.ts +810 -57
  168. package/src/sync/utils/downloadByRange.ts +256 -39
  169. package/src/sync/utils/downloadByRoot.ts +12 -2
  170. package/src/sync/utils/pendingBlocksTree.ts +0 -15
@@ -1,12 +1,14 @@
1
1
  import {ExecutionStatus, ProtoBlock} from "@lodestar/fork-choice";
2
- import {ForkName, isForkPostFulu} from "@lodestar/params";
2
+ import {ForkName, ForkSeq, isForkPostFulu} from "@lodestar/params";
3
3
  import {DataAvailabilityStatus, IBeaconStateView, computeEpochAtSlot} from "@lodestar/state-transition";
4
- import {IndexedAttestation, deneb} from "@lodestar/types";
4
+ import {IndexedAttestation, Slot, deneb} from "@lodestar/types";
5
+ import {getBlobKzgCommitments} from "../../util/dataColumns.js";
5
6
  import type {BeaconChain} from "../chain.js";
6
7
  import {BlockError, BlockErrorCode} from "../errors/index.js";
7
8
  import {BlockProcessOpts} from "../options.js";
8
9
  import {RegenCaller} from "../regen/index.js";
9
10
  import {DAType, IBlockInput} from "./blockInput/index.js";
11
+ import {PayloadEnvelopeInput} from "./payloadEnvelopeInput/payloadEnvelopeInput.js";
10
12
  import {ImportBlockOpts} from "./types.js";
11
13
  import {DENEB_BLOWFISH_BANNER} from "./utils/blowfishBanner.js";
12
14
  import {ELECTRA_GIRAFFE_BANNER} from "./utils/giraffeBanner.js";
@@ -16,6 +18,7 @@ import {verifyBlocksDataAvailability} from "./verifyBlocksDataAvailability.js";
16
18
  import {SegmentExecStatus, verifyBlocksExecutionPayload} from "./verifyBlocksExecutionPayloads.js";
17
19
  import {verifyBlocksSignatures} from "./verifyBlocksSignatures.js";
18
20
  import {verifyBlocksStateTransitionOnly} from "./verifyBlocksStateTransitionOnly.js";
21
+ import {verifyPayloadsDataAvailability} from "./verifyPayloadsDataAvailability.js";
19
22
 
20
23
  /**
21
24
  * Verifies 1 or more blocks are fully valid; from a linear sequence of blocks.
@@ -32,6 +35,7 @@ export async function verifyBlocksInEpoch(
32
35
  this: BeaconChain,
33
36
  parentBlock: ProtoBlock,
34
37
  blockInputs: IBlockInput[],
38
+ payloadEnvelopes: Map<Slot, PayloadEnvelopeInput> | null,
35
39
  opts: BlockProcessOpts & ImportBlockOpts
36
40
  ): Promise<{
37
41
  postStates: IBeaconStateView[];
@@ -110,6 +114,26 @@ export async function verifyBlocksInEpoch(
110
114
  });
111
115
  }
112
116
 
117
+ // Pick the data-availability source by fork:
118
+ // - Pre-Gloas: blob/Fulu-column data lives in IBlockInput → verifyBlocksDataAvailability.
119
+ // - Post-Gloas: verifyPayloadsDataAvailability
120
+ const daAvailabilityPromise =
121
+ fork >= ForkSeq.gloas
122
+ ? (async () => {
123
+ const payloadInputsForDa: PayloadEnvelopeInput[] = [];
124
+ for (const input of blockInputs) {
125
+ const pi = payloadEnvelopes?.get(input.slot);
126
+ if (pi !== undefined) payloadInputsForDa.push(pi);
127
+ }
128
+ await verifyPayloadsDataAvailability(payloadInputsForDa, abortController.signal);
129
+ return {
130
+ // post-gloas, DataAvailabilityStatus is NotRequired for forkChoice.onBlock() ProtoBlock
131
+ dataAvailabilityStatuses: blockInputs.map(() => DataAvailabilityStatus.NotRequired),
132
+ availableTime: Date.now(),
133
+ };
134
+ })()
135
+ : verifyBlocksDataAvailability(blockInputs, abortController.signal);
136
+
113
137
  // batch all I/O operations to reduce overhead
114
138
  const [
115
139
  segmentExecStatus,
@@ -119,8 +143,8 @@ export async function verifyBlocksInEpoch(
119
143
  ] = await Promise.all([
120
144
  verifyExecutionPayloadsPromise,
121
145
 
122
- // data availability for the blobs
123
- verifyBlocksDataAvailability(blockInputs, abortController.signal),
146
+ // data availability (fork-specific; see daAvailabilityPromise above)
147
+ daAvailabilityPromise,
124
148
 
125
149
  // Run state transition only
126
150
  // TODO: Ensure it yields to allow flushing to workers and engine API
@@ -149,6 +173,9 @@ export async function verifyBlocksInEpoch(
149
173
  opts
150
174
  )
151
175
  : Promise.resolve({verifySignaturesTime: Date.now()}),
176
+
177
+ // TODO GLOAS: can verify payload signatures in batch too
178
+ // maybe chain with the above verifyBlocksSignatures()
152
179
  ]);
153
180
 
154
181
  if (opts.verifyOnly !== true) {
@@ -200,7 +227,9 @@ export async function verifyBlocksInEpoch(
200
227
  blockInputs.length === 1 &&
201
228
  // gossip blocks have seenTimestampSec
202
229
  opts.seenTimestampSec !== undefined &&
230
+ // PreData (pre-deneb) and NoData (gloas) carry no blob data on the block — skip metric
203
231
  blockInputs[0].type !== DAType.PreData &&
232
+ blockInputs[0].type !== DAType.NoData &&
204
233
  executionStatuses[0] === ExecutionStatus.Valid
205
234
  ) {
206
235
  // Find the max time when the block was actually verified
@@ -209,8 +238,8 @@ export async function verifyBlocksInEpoch(
209
238
  this.metrics?.gossipBlock.receivedToFullyVerifiedTime.observe(recvTofullyVerifedTime);
210
239
 
211
240
  const verifiedToBlobsAvailabiltyTime = Math.max(availableTime - fullyVerifiedTime, 0) / 1000;
212
- const block = blockInputs[0].getBlock() as deneb.SignedBeaconBlock;
213
- const numBlobs = block.message.body.blobKzgCommitments.length;
241
+ const block = blockInputs[0].getBlock();
242
+ const numBlobs = getBlobKzgCommitments(blockInputs[0].forkName, block as deneb.SignedBeaconBlock).length;
214
243
 
215
244
  this.metrics?.gossipBlock.verifiedToBlobsAvailabiltyTime.observe({numBlobs}, verifiedToBlobsAvailabiltyTime);
216
245
  this.logger.verbose("Verified blockInput fully with blobs availability", {
@@ -90,15 +90,24 @@ export function verifyBlocksSanityChecks(
90
90
  } else {
91
91
  // When importing a block segment, only the first NON-IGNORED block must be known to the fork-choice.
92
92
  const parentRoot = toRootHex(block.message.parentRoot);
93
- parentBlock = isGloasBeaconBlock(block.message)
94
- ? chain.forkChoice.getBlockHexAndBlockHash(
95
- parentRoot,
96
- toRootHex(block.message.body.signedExecutionPayloadBid.message.parentBlockHash)
97
- )
98
- : chain.forkChoice.getBlockHexDefaultStatus(parentRoot);
99
- if (!parentBlock) {
93
+ const parentBlockDefaultStatus = chain.forkChoice.getBlockHexDefaultStatus(parentRoot);
94
+ if (!parentBlockDefaultStatus) {
100
95
  throw new BlockError(block, {code: BlockErrorCode.PARENT_UNKNOWN, parentRoot});
101
96
  }
97
+
98
+ parentBlock = parentBlockDefaultStatus;
99
+ if (isGloasBeaconBlock(block.message)) {
100
+ const parentBlockHash = toRootHex(block.message.body.signedExecutionPayloadBid.message.parentBlockHash);
101
+ const parentBlockWithPayload = chain.forkChoice.getBlockHexAndBlockHash(parentRoot, parentBlockHash);
102
+ if (!parentBlockWithPayload) {
103
+ throw new BlockError(block, {
104
+ code: BlockErrorCode.PARENT_PAYLOAD_UNKNOWN,
105
+ parentRoot,
106
+ parentBlockHash,
107
+ });
108
+ }
109
+ parentBlock = parentBlockWithPayload;
110
+ }
102
111
  // Parent is known to the fork-choice
103
112
  parentBlockSlot = parentBlock.slot;
104
113
  }
@@ -0,0 +1,129 @@
1
+ import {BeaconConfig} from "@lodestar/config";
2
+ import {
3
+ type IBeaconStateViewGloas,
4
+ type PubkeyCache,
5
+ computeTimeAtSlot,
6
+ getExecutionPayloadEnvelopeSignatureSet,
7
+ } from "@lodestar/state-transition";
8
+ import {gloas, ssz} from "@lodestar/types";
9
+ import {byteArrayEquals, toHex, toRootHex} from "@lodestar/utils";
10
+ import {IBlsVerifier} from "../bls/index.js";
11
+
12
+ export type VerifyExecutionPayloadEnvelopeOpts = {
13
+ verifyExecutionRequestsRoot?: boolean;
14
+ };
15
+
16
+ /**
17
+ * Verify execution payload envelope fields against the post-block state.
18
+ *
19
+ * Signature verification and the execution engine call (`verify_and_notify_new_payload`) are
20
+ * performed outside this function, see `verifyExecutionPayloadEnvelopeSignature` and
21
+ * `importExecutionPayload` which run both in parallel with this check.
22
+ *
23
+ * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.5/specs/gloas/fork-choice.md#new-verify_execution_payload_envelope
24
+ */
25
+ export function verifyExecutionPayloadEnvelope(
26
+ config: BeaconConfig,
27
+ state: IBeaconStateViewGloas,
28
+ envelope: gloas.ExecutionPayloadEnvelope,
29
+ opts?: VerifyExecutionPayloadEnvelopeOpts
30
+ ): void {
31
+ const {verifyExecutionRequestsRoot = true} = opts ?? {};
32
+ const payload = envelope.payload;
33
+
34
+ // Verify consistency with the beacon block.
35
+ // Compute header root on a clone of latestBlockHeader to avoid mutating state.
36
+ const headerValue = ssz.phase0.BeaconBlockHeader.clone(state.latestBlockHeader);
37
+ if (byteArrayEquals(headerValue.stateRoot, ssz.Root.defaultValue())) {
38
+ headerValue.stateRoot = state.hashTreeRoot();
39
+ }
40
+ const headerRoot = ssz.phase0.BeaconBlockHeader.hashTreeRoot(headerValue);
41
+ if (!byteArrayEquals(envelope.beaconBlockRoot, headerRoot)) {
42
+ throw new Error(
43
+ `Envelope's block is not the latest block header envelope=${toRootHex(envelope.beaconBlockRoot)} latestBlockHeader=${toRootHex(headerRoot)}`
44
+ );
45
+ }
46
+
47
+ // Verify consistency with the committed bid
48
+ const bid = state.latestExecutionPayloadBid;
49
+ if (envelope.builderIndex !== bid.builderIndex) {
50
+ throw new Error(
51
+ `Builder index mismatch between envelope and committed bid envelope=${envelope.builderIndex} bid=${bid.builderIndex}`
52
+ );
53
+ }
54
+ if (!byteArrayEquals(bid.prevRandao, payload.prevRandao)) {
55
+ throw new Error(
56
+ `Prev randao mismatch between bid and payload bid=${toHex(bid.prevRandao)} payload=${toHex(payload.prevRandao)}`
57
+ );
58
+ }
59
+ if (Number(bid.gasLimit) !== payload.gasLimit) {
60
+ throw new Error(
61
+ `Gas limit mismatch between payload and bid payload=${payload.gasLimit} bid=${Number(bid.gasLimit)}`
62
+ );
63
+ }
64
+ if (!byteArrayEquals(bid.blockHash, payload.blockHash)) {
65
+ throw new Error(
66
+ `Block hash mismatch between payload and bid payload=${toRootHex(payload.blockHash)} bid=${toRootHex(bid.blockHash)}`
67
+ );
68
+ }
69
+ // Verify execution_requests_root matches bid commitment.
70
+ // Can be skipped if already verified during gossip validation.
71
+ if (verifyExecutionRequestsRoot) {
72
+ const requestsRoot = ssz.electra.ExecutionRequests.hashTreeRoot(envelope.executionRequests);
73
+ if (!byteArrayEquals(requestsRoot, bid.executionRequestsRoot)) {
74
+ throw new Error(
75
+ `Execution requests root mismatch envelope=${toRootHex(requestsRoot)} bid=${toRootHex(bid.executionRequestsRoot)}`
76
+ );
77
+ }
78
+ }
79
+
80
+ // Verify the execution payload is valid
81
+ if (payload.slotNumber !== state.slot) {
82
+ throw new Error(`Slot mismatch between payload and state payload=${payload.slotNumber} state=${state.slot}`);
83
+ }
84
+ if (!byteArrayEquals(payload.parentHash, state.latestBlockHash)) {
85
+ throw new Error(
86
+ `Parent hash mismatch between payload and state payload=${toRootHex(payload.parentHash)} state=${toRootHex(state.latestBlockHash)}`
87
+ );
88
+ }
89
+ const expectedTimestamp = computeTimeAtSlot(config, state.slot, state.genesisTime);
90
+ if (payload.timestamp !== expectedTimestamp) {
91
+ throw new Error(
92
+ `Timestamp mismatch between payload and state payload=${payload.timestamp} state=${expectedTimestamp}`
93
+ );
94
+ }
95
+
96
+ // Verify consistency with expected withdrawals
97
+ const payloadWithdrawalsRoot = ssz.capella.Withdrawals.hashTreeRoot(payload.withdrawals);
98
+ const expectedWithdrawalsRoot = ssz.capella.Withdrawals.hashTreeRoot(state.payloadExpectedWithdrawals);
99
+ if (!byteArrayEquals(payloadWithdrawalsRoot, expectedWithdrawalsRoot)) {
100
+ throw new Error(
101
+ `Withdrawals mismatch between payload and expected payload=${toRootHex(payloadWithdrawalsRoot)} expected=${toRootHex(expectedWithdrawalsRoot)}`
102
+ );
103
+ }
104
+
105
+ // Execution engine verification (verify_and_notify_new_payload) is done externally by the caller
106
+ }
107
+
108
+ /**
109
+ * Verify the BLS signature of an execution payload envelope.
110
+ *
111
+ * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.5/specs/gloas/fork-choice.md#new-verify_execution_payload_envelope_signature
112
+ */
113
+ export async function verifyExecutionPayloadEnvelopeSignature(
114
+ config: BeaconConfig,
115
+ state: IBeaconStateViewGloas,
116
+ pubkeyCache: PubkeyCache,
117
+ signedEnvelope: gloas.SignedExecutionPayloadEnvelope,
118
+ proposerIndex: number,
119
+ bls: IBlsVerifier
120
+ ): Promise<boolean> {
121
+ const signatureSet = getExecutionPayloadEnvelopeSignatureSet(
122
+ config,
123
+ pubkeyCache,
124
+ state,
125
+ signedEnvelope,
126
+ proposerIndex
127
+ );
128
+ return bls.verifySignatureSets([signatureSet]);
129
+ }
@@ -5,7 +5,7 @@ import {writeDataColumnsToDb} from "./writeBlockInputToDb.js";
5
5
  /**
6
6
  * Persists payload envelope data to DB. This operation must be eventually completed if a payload is imported.
7
7
  *
8
- * TODO GLOAS: Persist envelope metadata (stateRoot, executionRequests, builderIndex, etc.) without the full
8
+ * TODO GLOAS: Persist envelope metadata (executionRequests, builderIndex, etc.) without the full
9
9
  * execution payload body — only keep the blockHash reference. The EL already stores the payload.
10
10
  * See https://github.com/ChainSafe/lodestar/issues/5671
11
11
  */
@@ -33,23 +33,14 @@ export async function persistPayloadEnvelopeInput(
33
33
  this: BeaconChain,
34
34
  payloadInput: PayloadEnvelopeInput
35
35
  ): Promise<void> {
36
- await writePayloadEnvelopeInputToDb
37
- .call(this, payloadInput)
38
- .catch((e) => {
39
- this.logger.error(
40
- "Error persisting payload envelope in hot db",
41
- {
42
- slot: payloadInput.slot,
43
- root: payloadInput.blockRootHex,
44
- },
45
- e
46
- );
47
- })
48
- .finally(() => {
49
- this.seenPayloadEnvelopeInputCache.prune(payloadInput.blockRootHex);
50
- this.logger.debug("Pruned payload envelope input", {
36
+ await writePayloadEnvelopeInputToDb.call(this, payloadInput).catch((e) => {
37
+ this.logger.error(
38
+ "Error persisting payload envelope in hot db",
39
+ {
51
40
  slot: payloadInput.slot,
52
41
  root: payloadInput.blockRootHex,
53
- });
54
- });
42
+ },
43
+ e
44
+ );
45
+ });
55
46
  }
@@ -39,6 +39,7 @@ import {
39
39
  ValidatorIndex,
40
40
  Wei,
41
41
  deneb,
42
+ electra,
42
43
  gloas,
43
44
  isBlindedBeaconBlock,
44
45
  phase0,
@@ -886,6 +887,21 @@ export class BeaconChain implements IBeaconChain {
886
887
  );
887
888
  }
888
889
 
890
+ async getParentExecutionRequests(
891
+ parentBlockSlot: Slot,
892
+ parentBlockRootHex: RootHex
893
+ ): Promise<electra.ExecutionRequests> {
894
+ // at the fork boundary, parent is pre-gloas
895
+ if (!isForkPostGloas(this.config.getForkName(parentBlockSlot))) {
896
+ return ssz.electra.ExecutionRequests.defaultValue();
897
+ }
898
+ const envelope = await this.getExecutionPayloadEnvelope(parentBlockSlot, parentBlockRootHex);
899
+ if (envelope === null) {
900
+ throw Error(`Parent execution payload envelope not found slot=${parentBlockSlot}, root=${parentBlockRootHex}`);
901
+ }
902
+ return envelope.message.executionRequests;
903
+ }
904
+
889
905
  async getDataColumnSidecars(blockSlot: Slot, blockRootHex: string): Promise<DataColumnSidecar[]> {
890
906
  const fork = this.config.getForkName(blockSlot);
891
907
 
@@ -1082,11 +1098,15 @@ export class BeaconChain implements IBeaconChain {
1082
1098
  }
1083
1099
 
1084
1100
  async processBlock(block: IBlockInput, opts?: ImportBlockOpts): Promise<void> {
1085
- return this.blockProcessor.processBlocksJob([block], opts);
1101
+ return this.blockProcessor.processBlocksJob([block], null, opts);
1086
1102
  }
1087
1103
 
1088
- async processChainSegment(blocks: IBlockInput[], opts?: ImportBlockOpts): Promise<void> {
1089
- return this.blockProcessor.processBlocksJob(blocks, opts);
1104
+ async processChainSegment(
1105
+ blocks: IBlockInput[],
1106
+ payloadEnvelopes: Map<Slot, PayloadEnvelopeInput> | null,
1107
+ opts?: ImportBlockOpts
1108
+ ): Promise<void> {
1109
+ await this.blockProcessor.processBlocksJob(blocks, payloadEnvelopes, opts);
1090
1110
  }
1091
1111
 
1092
1112
  async processExecutionPayload(payloadInput: PayloadEnvelopeInput, opts?: ImportPayloadOpts): Promise<void> {
@@ -74,6 +74,8 @@ export enum BlockErrorCode {
74
74
  PARENT_EXECUTION_INVALID = "BLOCK_ERROR_PARENT_EXECUTION_INVALID",
75
75
  /** The block's parent execution payload (defined by bid.parent_block_hash) has not been seen */
76
76
  PARENT_PAYLOAD_UNKNOWN = "BLOCK_ERROR_PARENT_PAYLOAD_UNKNOWN",
77
+ /** An execution payload envelope in the chain segment references a block root that does not match its slot's block */
78
+ ENVELOPE_BLOCK_ROOT_MISMATCH = "BLOCK_ERROR_ENVELOPE_BLOCK_ROOT_MISMATCH",
77
79
  }
78
80
 
79
81
  type ExecutionErrorStatus = Exclude<
@@ -107,6 +109,7 @@ export type BlockErrorType =
107
109
  | {code: BlockErrorCode.NOT_LATER_THAN_PARENT; parentSlot: Slot; slot: Slot}
108
110
  | {code: BlockErrorCode.NON_LINEAR_PARENT_ROOTS}
109
111
  | {code: BlockErrorCode.NON_LINEAR_SLOTS}
112
+ | {code: BlockErrorCode.ENVELOPE_BLOCK_ROOT_MISMATCH; envelopeBlockRoot: RootHex; blockRoot: RootHex}
110
113
  | {code: BlockErrorCode.PER_BLOCK_PROCESSING_ERROR; error: Error}
111
114
  | {code: BlockErrorCode.BEACON_CHAIN_ERROR; error: Error}
112
115
  | {code: BlockErrorCode.KNOWN_BAD_BLOCK}
@@ -120,7 +123,7 @@ export type BlockErrorType =
120
123
  | {code: BlockErrorCode.TOO_MANY_KZG_COMMITMENTS; blobKzgCommitmentsLen: number; commitmentLimit: number}
121
124
  | {code: BlockErrorCode.BID_PARENT_ROOT_MISMATCH; bidParentRoot: RootHex; blockParentRoot: RootHex}
122
125
  | {code: BlockErrorCode.PARENT_EXECUTION_INVALID; parentRoot: RootHex}
123
- | {code: BlockErrorCode.PARENT_PAYLOAD_UNKNOWN; parentBlockHash: RootHex};
126
+ | {code: BlockErrorCode.PARENT_PAYLOAD_UNKNOWN; parentRoot: RootHex; parentBlockHash: RootHex};
124
127
 
125
128
  export class BlockGossipError extends GossipActionError<BlockErrorType> {}
126
129
 
@@ -7,6 +7,7 @@ export enum ExecutionPayloadBidErrorCode {
7
7
  BID_ALREADY_KNOWN = "EXECUTION_PAYLOAD_BID_ERROR_BID_ALREADY_KNOWN",
8
8
  BID_TOO_LOW = "EXECUTION_PAYLOAD_BID_ERROR_BID_TOO_LOW",
9
9
  BID_TOO_HIGH = "EXECUTION_PAYLOAD_BID_ERROR_BID_TOO_HIGH",
10
+ TOO_MANY_KZG_COMMITMENTS = "EXECUTION_PAYLOAD_BID_ERROR_TOO_MANY_KZG_COMMITMENTS",
10
11
  UNKNOWN_BLOCK_ROOT = "EXECUTION_PAYLOAD_BID_ERROR_UNKNOWN_BLOCK_ROOT",
11
12
  INVALID_SLOT = "EXECUTION_PAYLOAD_BID_ERROR_INVALID_SLOT",
12
13
  INVALID_SIGNATURE = "EXECUTION_PAYLOAD_BID_ERROR_INVALID_SIGNATURE",
@@ -28,6 +29,11 @@ export type ExecutionPayloadBidErrorType =
28
29
  }
29
30
  | {code: ExecutionPayloadBidErrorCode.BID_TOO_LOW; bidValue: number; currentHighestBid: number}
30
31
  | {code: ExecutionPayloadBidErrorCode.BID_TOO_HIGH; bidValue: number; builderBalance: number}
32
+ | {
33
+ code: ExecutionPayloadBidErrorCode.TOO_MANY_KZG_COMMITMENTS;
34
+ blobKzgCommitmentsLen: number;
35
+ commitmentLimit: number;
36
+ }
31
37
  | {code: ExecutionPayloadBidErrorCode.UNKNOWN_BLOCK_ROOT; parentBlockRoot: RootHex}
32
38
  | {code: ExecutionPayloadBidErrorCode.INVALID_SLOT; builderIndex: BuilderIndex; slot: Slot}
33
39
  | {code: ExecutionPayloadBidErrorCode.INVALID_SIGNATURE; builderIndex: BuilderIndex; slot: Slot};
@@ -11,6 +11,7 @@ export enum ExecutionPayloadEnvelopeErrorCode {
11
11
  SLOT_MISMATCH = "EXECUTION_PAYLOAD_ENVELOPE_ERROR_SLOT_MISMATCH",
12
12
  BUILDER_INDEX_MISMATCH = "EXECUTION_PAYLOAD_ENVELOPE_ERROR_BUILDER_INDEX_MISMATCH",
13
13
  BLOCK_HASH_MISMATCH = "EXECUTION_PAYLOAD_ENVELOPE_ERROR_BLOCK_HASH_MISMATCH",
14
+ EXECUTION_REQUESTS_ROOT_MISMATCH = "EXECUTION_PAYLOAD_ENVELOPE_ERROR_EXECUTION_REQUESTS_ROOT_MISMATCH",
14
15
  INVALID_SIGNATURE = "EXECUTION_PAYLOAD_ENVELOPE_ERROR_INVALID_SIGNATURE",
15
16
  PAYLOAD_ENVELOPE_INPUT_MISSING = "EXECUTION_PAYLOAD_ENVELOPE_ERROR_PAYLOAD_ENVELOPE_INPUT_MISSING",
16
17
  }
@@ -36,6 +37,11 @@ export type ExecutionPayloadEnvelopeErrorType =
36
37
  envelopeBlockHash: RootHex;
37
38
  bidBlockHash: RootHex | null;
38
39
  }
40
+ | {
41
+ code: ExecutionPayloadEnvelopeErrorCode.EXECUTION_REQUESTS_ROOT_MISMATCH;
42
+ envelopeRequestsRoot: RootHex;
43
+ bidRequestsRoot: RootHex;
44
+ }
39
45
  | {code: ExecutionPayloadEnvelopeErrorCode.INVALID_SIGNATURE}
40
46
  | {code: ExecutionPayloadEnvelopeErrorCode.PAYLOAD_ENVELOPE_INPUT_MISSING; blockRoot: RootHex};
41
47
 
@@ -148,7 +148,7 @@ export function initializeForkChoiceFromFinalizedState(
148
148
  : {executionPayloadBlockHash: null, executionStatus: ExecutionStatus.PreMerge}),
149
149
 
150
150
  dataAvailabilityStatus: DataAvailabilityStatus.PreData,
151
- payloadStatus: isForkPostGloas ? PayloadStatus.PENDING : PayloadStatus.FULL, // TODO GLOAS: Post-gloas how do we know if the checkpoint payload is FULL or EMPTY?
151
+ payloadStatus: isForkPostGloas ? PayloadStatus.PENDING : PayloadStatus.FULL,
152
152
  parentBlockHash: isStatePostGloas(state) ? toRootHex(state.latestBlockHash) : null,
153
153
  },
154
154
  currentSlot
@@ -240,7 +240,7 @@ export function initializeForkChoiceFromUnfinalizedState(
240
240
  : {executionPayloadBlockHash: null, executionStatus: ExecutionStatus.PreMerge}),
241
241
 
242
242
  dataAvailabilityStatus: DataAvailabilityStatus.PreData,
243
- payloadStatus: isForkPostGloas ? PayloadStatus.PENDING : PayloadStatus.FULL, // TODO GLOAS: Post-gloas how do we know if the checkpoint payload is FULL or EMPTY?
243
+ payloadStatus: isForkPostGloas ? PayloadStatus.PENDING : PayloadStatus.FULL,
244
244
  parentBlockHash: isStatePostGloas(unfinalizedState) ? toRootHex(unfinalizedState.latestBlockHash) : null,
245
245
  };
246
246
 
@@ -18,6 +18,7 @@ import {
18
18
  altair,
19
19
  capella,
20
20
  deneb,
21
+ electra,
21
22
  gloas,
22
23
  phase0,
23
24
  rewards,
@@ -231,6 +232,7 @@ export interface IBeaconChain {
231
232
  blockSlot: Slot,
232
233
  blockRootHex: string
233
234
  ): Promise<gloas.SignedExecutionPayloadEnvelope | null>;
235
+ getParentExecutionRequests(parentBlockSlot: Slot, parentBlockRootHex: RootHex): Promise<electra.ExecutionRequests>;
234
236
 
235
237
  produceCommonBlockBody(blockAttributes: BlockAttributes): Promise<CommonBlockBody>;
236
238
  produceBlock(blockAttributes: BlockAttributes & {commonBlockBodyPromise: Promise<CommonBlockBody>}): Promise<{
@@ -248,7 +250,11 @@ export interface IBeaconChain {
248
250
  /** Process a block until complete */
249
251
  processBlock(block: IBlockInput, opts?: ImportBlockOpts): Promise<void>;
250
252
  /** Process a chain of blocks until complete */
251
- processChainSegment(blocks: IBlockInput[], opts?: ImportBlockOpts): Promise<void>;
253
+ processChainSegment(
254
+ blocks: IBlockInput[],
255
+ payloadEnvelopes: Map<Slot, PayloadEnvelopeInput> | null,
256
+ opts?: ImportBlockOpts
257
+ ): Promise<void>;
252
258
 
253
259
  /** Process execution payload envelope: verify, import to fork choice, and persist to DB */
254
260
  processExecutionPayload(payloadInput: PayloadEnvelopeInput, opts?: ImportPayloadOpts): Promise<void>;
@@ -10,7 +10,7 @@ import {
10
10
  isStatePostBellatrix,
11
11
  isStatePostGloas,
12
12
  } from "@lodestar/state-transition";
13
- import {Bytes32, Slot} from "@lodestar/types";
13
+ import {Bytes32, Slot, electra} from "@lodestar/types";
14
14
  import {Logger, fromHex, isErrorAborted, sleep} from "@lodestar/utils";
15
15
  import {GENESIS_SLOT, ZERO_HASH_HEX} from "../constants/constants.js";
16
16
  import {BuilderStatus} from "../execution/builder/http.js";
@@ -83,7 +83,7 @@ export class PrepareNextSlotScheduler {
83
83
  const headBlock = this.chain.recomputeForkChoiceHead(ForkchoiceCaller.prepareNextSlot);
84
84
  const {slot: headSlot, blockRoot: headRoot} = headBlock;
85
85
  // may be updated below if we predict a proposer-boost-reorg
86
- let updatedHeadRoot = headRoot;
86
+ let updatedHead = headBlock;
87
87
 
88
88
  // PS: previously this was comparing slots, but that gave no leway on the skipped
89
89
  // slots on epoch bounday. Making it more fluid.
@@ -148,7 +148,7 @@ export class PrepareNextSlotScheduler {
148
148
  {dontTransferCache: !isEpochTransition},
149
149
  RegenCaller.predictProposerHead
150
150
  );
151
- updatedHeadRoot = proposerHeadRoot;
151
+ updatedHead = proposerHead;
152
152
  }
153
153
 
154
154
  // Update the builder status, if enabled shoot an api call to check status
@@ -165,14 +165,19 @@ export class PrepareNextSlotScheduler {
165
165
  }
166
166
 
167
167
  let parentBlockHash: Bytes32;
168
+ let isExtendingPayload = false;
168
169
  if (isStatePostGloas(updatedPrepareState)) {
169
- parentBlockHash = this.chain.forkChoice.shouldExtendPayload(updatedHeadRoot)
170
+ isExtendingPayload = this.chain.forkChoice.shouldExtendPayload(updatedHead.blockRoot);
171
+ parentBlockHash = isExtendingPayload
170
172
  ? updatedPrepareState.latestExecutionPayloadBid.blockHash
171
173
  : updatedPrepareState.latestExecutionPayloadBid.parentBlockHash;
172
174
  } else {
173
175
  parentBlockHash = updatedPrepareState.latestExecutionPayloadHeader.blockHash;
174
176
  }
175
177
 
178
+ // Reused by the SSE emit below to avoid a second DB lookup on cache miss
179
+ let parentExecutionRequests: electra.ExecutionRequests | undefined;
180
+
176
181
  if (feeRecipient) {
177
182
  const preparationTime =
178
183
  computeTimeAtSlot(this.config, prepareSlot, this.chain.genesisTime) - Date.now() / 1000;
@@ -182,6 +187,13 @@ export class PrepareNextSlotScheduler {
182
187
  const finalizedBlockHash =
183
188
  this.chain.forkChoice.getFinalizedBlock().executionPayloadBlockHash ?? ZERO_HASH_HEX;
184
189
 
190
+ if (isExtendingPayload) {
191
+ parentExecutionRequests = await this.chain.getParentExecutionRequests(
192
+ updatedHead.slot,
193
+ updatedHead.blockRoot
194
+ );
195
+ }
196
+
185
197
  // awaiting here instead of throwing an async call because there is no other task
186
198
  // left for scheduler and this gives nice semantics to catch and log errors in the
187
199
  // try/catch wrapper here.
@@ -189,12 +201,13 @@ export class PrepareNextSlotScheduler {
189
201
  this.chain,
190
202
  this.logger,
191
203
  fork as ForkPostBellatrix, // State is of execution type
192
- fromHex(updatedHeadRoot),
204
+ fromHex(updatedHead.blockRoot),
193
205
  parentBlockHash,
194
206
  safeBlockHash,
195
207
  finalizedBlockHash,
196
208
  updatedPrepareState,
197
- feeRecipient
209
+ feeRecipient,
210
+ parentExecutionRequests
198
211
  );
199
212
  this.logger.verbose("PrepareNextSlotScheduler prepared new payload", {
200
213
  prepareSlot,
@@ -203,21 +216,38 @@ export class PrepareNextSlotScheduler {
203
216
  });
204
217
  }
205
218
 
219
+ if (ForkSeq[fork] >= ForkSeq.gloas) {
220
+ // Cutoff = slot of the parent of the block we'll actually build on (post-reorg).
221
+ // Steady state: cache holds just 2 entries — head (parent for next-slot production)
222
+ // and head.parent (proposer-boost-reorg fallback). Anything older is evicted.
223
+ const updatedHeadParent = this.chain.forkChoice.getBlockHexDefaultStatus(updatedHead.parentRoot);
224
+ if (updatedHeadParent) {
225
+ this.chain.seenPayloadEnvelopeInputCache.pruneBelow(updatedHeadParent.slot);
226
+ }
227
+ }
228
+
206
229
  this.computeStateHashTreeRoot(updatedPrepareState, isEpochTransition);
207
230
 
208
- // If emitPayloadAttributes is true emit a SSE payloadAttributes event
231
+ // If emitPayloadAttributes is true emit a SSE payloadAttributes event for
232
+ // every slot. Without the flag, only emit the event if we are proposing in the next slot.
209
233
  if (
210
- this.chain.opts.emitPayloadAttributes === true &&
234
+ (feeRecipient || this.chain.opts.emitPayloadAttributes === true) &&
211
235
  this.chain.emitter.listenerCount(routes.events.EventType.payloadAttributes)
212
236
  ) {
237
+ // if we didn't fetch above (not proposing), SSE still needs it here
238
+ if (!parentExecutionRequests && isExtendingPayload) {
239
+ parentExecutionRequests = await this.chain.getParentExecutionRequests(
240
+ updatedHead.slot,
241
+ updatedHead.blockRoot
242
+ );
243
+ }
213
244
  const data = getPayloadAttributesForSSE(fork as ForkPostBellatrix, this.chain, {
214
245
  prepareState: updatedPrepareState,
215
246
  prepareSlot,
216
- parentBlockRoot: fromHex(headRoot),
247
+ parentBlockRoot: fromHex(updatedHead.blockRoot),
217
248
  parentBlockHash,
218
- // The likely consumers of this API are builders and will anyway ignore the
219
- // feeRecipient, so just pass zero hash for now till a real use case arises
220
- feeRecipient: "0x0000000000000000000000000000000000000000000000000000000000000000",
249
+ feeRecipient: feeRecipient ?? "0x0000000000000000000000000000000000000000",
250
+ parentExecutionRequests,
221
251
  });
222
252
  this.chain.emitter.emit(routes.events.EventType.payloadAttributes, {data, version: fork});
223
253
  }