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

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 (67) hide show
  1. package/lib/chain/blocks/importExecutionPayload.d.ts +14 -13
  2. package/lib/chain/blocks/importExecutionPayload.d.ts.map +1 -1
  3. package/lib/chain/blocks/importExecutionPayload.js +59 -74
  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 +0 -1
  7. package/lib/chain/blocks/index.js.map +1 -1
  8. package/lib/chain/blocks/types.d.ts +14 -20
  9. package/lib/chain/blocks/types.d.ts.map +1 -1
  10. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.d.ts +24 -0
  11. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.d.ts.map +1 -0
  12. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js +76 -0
  13. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js.map +1 -0
  14. package/lib/chain/blocks/writePayloadEnvelopeInputToDb.d.ts +1 -1
  15. package/lib/chain/blocks/writePayloadEnvelopeInputToDb.d.ts.map +1 -1
  16. package/lib/chain/blocks/writePayloadEnvelopeInputToDb.js +2 -11
  17. package/lib/chain/blocks/writePayloadEnvelopeInputToDb.js.map +1 -1
  18. package/lib/chain/chain.d.ts +2 -1
  19. package/lib/chain/chain.d.ts.map +1 -1
  20. package/lib/chain/chain.js +7 -0
  21. package/lib/chain/chain.js.map +1 -1
  22. package/lib/chain/errors/executionPayloadBid.d.ts +5 -0
  23. package/lib/chain/errors/executionPayloadBid.d.ts.map +1 -1
  24. package/lib/chain/errors/executionPayloadBid.js +1 -0
  25. package/lib/chain/errors/executionPayloadBid.js.map +1 -1
  26. package/lib/chain/errors/executionPayloadEnvelope.d.ts +5 -0
  27. package/lib/chain/errors/executionPayloadEnvelope.d.ts.map +1 -1
  28. package/lib/chain/errors/executionPayloadEnvelope.js +1 -0
  29. package/lib/chain/errors/executionPayloadEnvelope.js.map +1 -1
  30. package/lib/chain/forkChoice/index.js +2 -2
  31. package/lib/chain/forkChoice/index.js.map +1 -1
  32. package/lib/chain/interface.d.ts +2 -1
  33. package/lib/chain/interface.d.ts.map +1 -1
  34. package/lib/chain/interface.js.map +1 -1
  35. package/lib/chain/prepareNextSlot.d.ts.map +1 -1
  36. package/lib/chain/prepareNextSlot.js +30 -10
  37. package/lib/chain/prepareNextSlot.js.map +1 -1
  38. package/lib/chain/produceBlock/produceBlockBody.d.ts +3 -2
  39. package/lib/chain/produceBlock/produceBlockBody.d.ts.map +1 -1
  40. package/lib/chain/produceBlock/produceBlockBody.js +27 -12
  41. package/lib/chain/produceBlock/produceBlockBody.js.map +1 -1
  42. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts +11 -4
  43. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts.map +1 -1
  44. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js +20 -18
  45. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js.map +1 -1
  46. package/lib/chain/validation/executionPayloadBid.d.ts.map +1 -1
  47. package/lib/chain/validation/executionPayloadBid.js +13 -1
  48. package/lib/chain/validation/executionPayloadBid.js.map +1 -1
  49. package/lib/chain/validation/executionPayloadEnvelope.d.ts.map +1 -1
  50. package/lib/chain/validation/executionPayloadEnvelope.js +11 -1
  51. package/lib/chain/validation/executionPayloadEnvelope.js.map +1 -1
  52. package/package.json +16 -15
  53. package/src/chain/blocks/importExecutionPayload.ts +73 -93
  54. package/src/chain/blocks/index.ts +0 -1
  55. package/src/chain/blocks/types.ts +14 -25
  56. package/src/chain/blocks/verifyExecutionPayloadEnvelope.ts +129 -0
  57. package/src/chain/blocks/writePayloadEnvelopeInputToDb.ts +9 -18
  58. package/src/chain/chain.ts +12 -0
  59. package/src/chain/errors/executionPayloadBid.ts +6 -0
  60. package/src/chain/errors/executionPayloadEnvelope.ts +6 -0
  61. package/src/chain/forkChoice/index.ts +2 -2
  62. package/src/chain/interface.ts +2 -0
  63. package/src/chain/prepareNextSlot.ts +42 -12
  64. package/src/chain/produceBlock/produceBlockBody.ts +30 -10
  65. package/src/chain/seenCache/seenPayloadEnvelopeInput.ts +22 -20
  66. package/src/chain/validation/executionPayloadBid.ts +14 -0
  67. package/src/chain/validation/executionPayloadEnvelope.ts +12 -2
package/package.json CHANGED
@@ -11,7 +11,7 @@
11
11
  "bugs": {
12
12
  "url": "https://github.com/ChainSafe/lodestar/issues"
13
13
  },
14
- "version": "1.43.0-dev.ca1fc40294",
14
+ "version": "1.43.0-dev.d166e3b6f7",
15
15
  "type": "module",
16
16
  "exports": {
17
17
  ".": {
@@ -135,18 +135,18 @@
135
135
  "@libp2p/peer-id": "^6.0.4",
136
136
  "@libp2p/prometheus-metrics": "^5.0.14",
137
137
  "@libp2p/tcp": "^11.0.13",
138
- "@lodestar/api": "^1.43.0-dev.ca1fc40294",
139
- "@lodestar/config": "^1.43.0-dev.ca1fc40294",
140
- "@lodestar/db": "^1.43.0-dev.ca1fc40294",
141
- "@lodestar/fork-choice": "^1.43.0-dev.ca1fc40294",
142
- "@lodestar/light-client": "^1.43.0-dev.ca1fc40294",
143
- "@lodestar/logger": "^1.43.0-dev.ca1fc40294",
144
- "@lodestar/params": "^1.43.0-dev.ca1fc40294",
145
- "@lodestar/reqresp": "^1.43.0-dev.ca1fc40294",
146
- "@lodestar/state-transition": "^1.43.0-dev.ca1fc40294",
147
- "@lodestar/types": "^1.43.0-dev.ca1fc40294",
148
- "@lodestar/utils": "^1.43.0-dev.ca1fc40294",
149
- "@lodestar/validator": "^1.43.0-dev.ca1fc40294",
138
+ "@lodestar/api": "^1.43.0-dev.d166e3b6f7",
139
+ "@lodestar/config": "^1.43.0-dev.d166e3b6f7",
140
+ "@lodestar/db": "^1.43.0-dev.d166e3b6f7",
141
+ "@lodestar/fork-choice": "^1.43.0-dev.d166e3b6f7",
142
+ "@lodestar/light-client": "^1.43.0-dev.d166e3b6f7",
143
+ "@lodestar/logger": "^1.43.0-dev.d166e3b6f7",
144
+ "@lodestar/params": "^1.43.0-dev.d166e3b6f7",
145
+ "@lodestar/reqresp": "^1.43.0-dev.d166e3b6f7",
146
+ "@lodestar/state-transition": "^1.43.0-dev.d166e3b6f7",
147
+ "@lodestar/types": "^1.43.0-dev.d166e3b6f7",
148
+ "@lodestar/utils": "^1.43.0-dev.d166e3b6f7",
149
+ "@lodestar/validator": "^1.43.0-dev.d166e3b6f7",
150
150
  "@multiformats/multiaddr": "^13.0.1",
151
151
  "datastore-core": "^11.0.2",
152
152
  "datastore-fs": "^11.0.2",
@@ -169,10 +169,11 @@
169
169
  "@libp2p/interface-internal": "^3.0.13",
170
170
  "@libp2p/logger": "^6.2.2",
171
171
  "@libp2p/utils": "^7.0.13",
172
- "@lodestar/spec-test-util": "^1.43.0-dev.ca1fc40294",
172
+ "@lodestar/spec-test-util": "^1.43.0-dev.d166e3b6f7",
173
173
  "@types/js-yaml": "^4.0.5",
174
174
  "@types/qs": "^6.9.7",
175
175
  "@types/tmp": "^0.2.3",
176
+ "dotenv": "^16.4.5",
176
177
  "js-yaml": "^4.1.0",
177
178
  "rewiremock": "^3.14.5",
178
179
  "rimraf": "^4.4.1",
@@ -186,5 +187,5 @@
186
187
  "beacon",
187
188
  "blockchain"
188
189
  ],
189
- "gitHead": "b9b4b6a67243378ab112c8d86a5681931be9bcc9"
190
+ "gitHead": "d66d8c5f6a57c9b358cf85a6fb113ca519705b29"
190
191
  }
@@ -1,14 +1,17 @@
1
1
  import {routes} from "@lodestar/api";
2
2
  import {ExecutionStatus, PayloadExecutionStatus} from "@lodestar/fork-choice";
3
- import {SLOTS_PER_EPOCH} from "@lodestar/params";
4
- import {getExecutionPayloadEnvelopeSignatureSet, isStatePostGloas} from "@lodestar/state-transition";
5
- import {fromHex, toRootHex} from "@lodestar/utils";
3
+ import {isStatePostGloas} from "@lodestar/state-transition";
4
+ import {fromHex} from "@lodestar/utils";
6
5
  import {ExecutionPayloadStatus} from "../../execution/index.js";
7
6
  import {isQueueErrorAborted} from "../../util/queue/index.js";
8
7
  import {BeaconChain} from "../chain.js";
9
8
  import {RegenCaller} from "../regen/interface.js";
10
9
  import {PayloadEnvelopeInput} from "../seenCache/seenPayloadEnvelopeInput.js";
11
10
  import {ImportPayloadOpts} from "./types.js";
11
+ import {
12
+ verifyExecutionPayloadEnvelope,
13
+ verifyExecutionPayloadEnvelopeSignature,
14
+ } from "./verifyExecutionPayloadEnvelope.js";
12
15
  import {verifyPayloadsDataAvailability} from "./verifyPayloadsDataAvailability.js";
13
16
 
14
17
  const EVENTSTREAM_EMIT_RECENT_EXECUTION_PAYLOAD_SLOTS = 64;
@@ -17,7 +20,7 @@ export enum PayloadErrorCode {
17
20
  EXECUTION_ENGINE_INVALID = "PAYLOAD_ERROR_EXECUTION_ENGINE_INVALID",
18
21
  EXECUTION_ENGINE_ERROR = "PAYLOAD_ERROR_EXECUTION_ENGINE_ERROR",
19
22
  BLOCK_NOT_IN_FORK_CHOICE = "PAYLOAD_ERROR_BLOCK_NOT_IN_FORK_CHOICE",
20
- STATE_TRANSITION_ERROR = "PAYLOAD_ERROR_STATE_TRANSITION_ERROR",
23
+ ENVELOPE_VERIFICATION_ERROR = "PAYLOAD_ERROR_ENVELOPE_VERIFICATION_ERROR",
21
24
  INVALID_SIGNATURE = "PAYLOAD_ERROR_INVALID_SIGNATURE",
22
25
  }
23
26
 
@@ -37,7 +40,7 @@ export type PayloadErrorType =
37
40
  blockRootHex: string;
38
41
  }
39
42
  | {
40
- code: PayloadErrorCode.STATE_TRANSITION_ERROR;
43
+ code: PayloadErrorCode.ENVELOPE_VERIFICATION_ERROR;
41
44
  message: string;
42
45
  }
43
46
  | {
@@ -69,18 +72,19 @@ function toForkChoiceExecutionStatus(status: ExecutionPayloadStatus): PayloadExe
69
72
  /**
70
73
  * Import an execution payload envelope after all data is available.
71
74
  *
72
- * This function:
73
- * 1. Emits `execution_payload_available` if payload is for current slot
74
- * 2. Gets the ProtoBlock from fork choice
75
- * 3. Applies write-queue backpressure (waitForSpace) early, before verification
76
- * 4. Regenerates the block state
77
- * 5. Runs EL verification (notifyNewPayload) in parallel with signature verification and processExecutionPayloadEnvelope
78
- * 6. Persists verified payload envelope to hot DB
79
- * 7. Updates fork choice
80
- * 8. Caches the post-execution payload state
81
- * 9. Records metrics for column sources
82
- * 10. Emits `execution_payload` for recent enough payloads after successful import
75
+ * The envelope is only verified here, no state mutation. State effects from the payload
76
+ * are applied on the next block via processParentExecutionPayload.
83
77
  *
78
+ * Steps:
79
+ * 1. Emit `execution_payload_available` event for payload attestation
80
+ * 2. Get the ProtoBlock from fork choice
81
+ * 3. Wait for data columns to be available
82
+ * 4. Regenerate state for envelope verification
83
+ * 5. Verify envelope (fields against state, signature, and EL in parallel where possible)
84
+ * 6. Persist verified payload envelope to hot DB (waits for write-queue space for backpressure)
85
+ * 7. Update fork choice (transitions the block's PENDING variant to FULL)
86
+ * 8. Record metrics for payload envelope and column sources
87
+ * 9. Emit `execution_payload` event
84
88
  */
85
89
  export async function importExecutionPayload(
86
90
  this: BeaconChain,
@@ -90,17 +94,18 @@ export async function importExecutionPayload(
90
94
  ): Promise<void> {
91
95
  const signedEnvelope = payloadInput.getPayloadEnvelope();
92
96
  const envelope = signedEnvelope.message;
97
+ const slot = envelope.payload.slotNumber;
93
98
  const blockRootHex = payloadInput.blockRootHex;
94
99
  const blockHashHex = payloadInput.getBlockHashHex();
95
- const fork = this.config.getForkName(envelope.payload.slotNumber);
100
+ const fork = this.config.getForkName(slot);
96
101
 
97
- // 1. Emit `execution_payload_available` event at the start of import. At this point the payload input
98
- // is already complete, so the payload and required data are available for payload attestation.
99
- // This event is only about availability, not validity of the execution payload, hence we can emit
100
- // it before getting a response from the execution client on whether the payload is valid or not.
101
- if (this.clock.currentSlot - envelope.payload.slotNumber < EVENTSTREAM_EMIT_RECENT_EXECUTION_PAYLOAD_SLOTS) {
102
+ // 1. Emit `execution_payload_available` event at the start of import. At this point the
103
+ // payload input is already complete, so the payload and required data are available for
104
+ // payload attestation. This event only signals availability (not validity), so we can emit
105
+ // it before getting a response from the EL on whether the payload is valid or not.
106
+ if (this.clock.currentSlot - slot < EVENTSTREAM_EMIT_RECENT_EXECUTION_PAYLOAD_SLOTS) {
102
107
  this.emitter.emit(routes.events.EventType.executionPayloadAvailable, {
103
- slot: envelope.payload.slotNumber,
108
+ slot,
104
109
  blockRoot: blockRootHex,
105
110
  });
106
111
  }
@@ -114,16 +119,11 @@ export async function importExecutionPayload(
114
119
  });
115
120
  }
116
121
 
117
- // 3. Wait for data columns to be available before claiming a write-queue slot.
122
+ // 3. Wait for data columns to be available.
118
123
  // The helper is shared with future gloas sync services; take the single-item batch form here.
119
124
  await verifyPayloadsDataAvailability([payloadInput], signal);
120
125
 
121
- // 4. Apply backpressure from the write queue, before doing verification work.
122
- // The actual DB write is deferred until after verification succeeds.
123
- await this.unfinalizedPayloadEnvelopeWrites.waitForSpace();
124
-
125
- // 5. Get pre-state for processExecutionPayloadEnvelope
126
- // We need the block state (post-block, pre-payload) to process the envelope
126
+ // 4. Regenerate state for envelope verification
127
127
  const blockState = await this.regen.getBlockSlotState(
128
128
  protoBlock,
129
129
  protoBlock.slot,
@@ -132,13 +132,30 @@ export async function importExecutionPayload(
132
132
  );
133
133
  if (!isStatePostGloas(blockState)) {
134
134
  throw new PayloadError({
135
- code: PayloadErrorCode.STATE_TRANSITION_ERROR,
136
- message: `Expected gloas+ block state for payload import, got fork=${blockState.forkName}`,
135
+ code: PayloadErrorCode.ENVELOPE_VERIFICATION_ERROR,
136
+ message: `Expected gloas+ state for payload import, got fork=${blockState.forkName}`,
137
+ });
138
+ }
139
+
140
+ // 5. Verify envelope fields against state first to fail fast before the EL + BLS work.
141
+ // When validSignature is true, gossip/API has already verified both the signature and the
142
+ // executionRequestsRoot, so we skip those checks here.
143
+ try {
144
+ verifyExecutionPayloadEnvelope(this.config, blockState, envelope, {
145
+ verifyExecutionRequestsRoot: !opts.validSignature,
137
146
  });
147
+ } catch (e) {
148
+ throw new PayloadError(
149
+ {
150
+ code: PayloadErrorCode.ENVELOPE_VERIFICATION_ERROR,
151
+ message: (e as Error).message,
152
+ },
153
+ `Envelope verification error: ${(e as Error).message}`
154
+ );
138
155
  }
139
156
 
140
- // 6. Run verification steps in parallel
141
- const [execResult, signatureValid, postPayloadResult] = await Promise.all([
157
+ // 5a. Run EL and signature verification in parallel
158
+ const [execResult, signatureValid] = await Promise.all([
142
159
  this.executionEngine.notifyNewPayload(
143
160
  fork,
144
161
  envelope.payload,
@@ -149,45 +166,22 @@ export async function importExecutionPayload(
149
166
 
150
167
  opts.validSignature === true
151
168
  ? Promise.resolve(true)
152
- : (async () => {
153
- const signatureSet = getExecutionPayloadEnvelopeSignatureSet(
154
- this.config,
155
- this.pubkeyCache,
156
- blockState,
157
- signedEnvelope,
158
- payloadInput.proposerIndex
159
- );
160
- return this.bls.verifySignatureSets([signatureSet]);
161
- })(),
162
-
163
- // Signature verified separately above.
164
- // State root check is done separately below with better error typing (matching block pipeline pattern).
165
- (async () => {
166
- try {
167
- return {
168
- postPayloadState: blockState.processExecutionPayloadEnvelope(signedEnvelope, {
169
- verifySignature: false,
170
- verifyStateRoot: false,
171
- }),
172
- };
173
- } catch (e) {
174
- throw new PayloadError(
175
- {
176
- code: PayloadErrorCode.STATE_TRANSITION_ERROR,
177
- message: (e as Error).message,
178
- },
179
- `State transition error: ${(e as Error).message}`
180
- );
181
- }
182
- })(),
169
+ : verifyExecutionPayloadEnvelopeSignature(
170
+ this.config,
171
+ blockState,
172
+ this.pubkeyCache,
173
+ signedEnvelope,
174
+ payloadInput.proposerIndex,
175
+ this.bls
176
+ ),
183
177
  ]);
184
178
 
185
- // 5a. Check signature verification result
179
+ // 5b. Check signature verification result
186
180
  if (!signatureValid) {
187
181
  throw new PayloadError({code: PayloadErrorCode.INVALID_SIGNATURE});
188
182
  }
189
183
 
190
- // 5b. Handle EL response
184
+ // 5c. Handle EL response
191
185
  switch (execResult.status) {
192
186
  case ExecutionPayloadStatus.VALID:
193
187
  break;
@@ -213,47 +207,33 @@ export async function importExecutionPayload(
213
207
  });
214
208
  }
215
209
 
216
- // 5c. Compute post-payload state root
217
- const postPayloadState = postPayloadResult.postPayloadState;
218
- const postPayloadStateRoot = postPayloadState.hashTreeRoot();
219
-
220
- // 6. Persist payload envelope to hot DB (performed asynchronously to avoid blocking)
210
+ // 6. Persist payload envelope to hot DB. Wait for write-queue space here to apply backpressure
211
+ // on the import pipeline during sync, then perform the write asynchronously to avoid blocking.
212
+ await this.unfinalizedPayloadEnvelopeWrites.waitForSpace();
221
213
  this.unfinalizedPayloadEnvelopeWrites.push(payloadInput).catch((e) => {
222
214
  if (!isQueueErrorAborted(e)) {
223
215
  this.logger.error(
224
216
  "Error pushing payload envelope to unfinalized write queue",
225
- {slot: envelope.payload.slotNumber, blockRoot: blockRootHex},
217
+ {slot, blockRoot: blockRootHex},
226
218
  e as Error
227
219
  );
228
220
  }
229
221
  });
230
222
 
231
- // 7. Update fork choice
232
- this.forkChoice.onExecutionPayload(
233
- blockRootHex,
234
- blockHashHex,
235
- envelope.payload.blockNumber,
236
- toRootHex(postPayloadStateRoot),
237
- toForkChoiceExecutionStatus(execResult.status)
238
- );
239
-
240
- // 8. Cache payload state
241
- this.regen.processState(blockRootHex, postPayloadState);
242
- if (postPayloadState.slot % SLOTS_PER_EPOCH === 0) {
243
- const {checkpoint} = postPayloadState.computeAnchorCheckpoint();
244
- this.regen.addCheckpointState(checkpoint, postPayloadState);
245
- }
223
+ // 7. Update fork choice, transitions the block's PENDING variant to FULL
224
+ const execStatus = toForkChoiceExecutionStatus(execResult.status);
225
+ this.forkChoice.onExecutionPayload(blockRootHex, blockHashHex, envelope.payload.blockNumber, execStatus);
246
226
 
247
- // 9. Record metrics for payload envelope and column sources
227
+ // 8. Record metrics for payload envelope and column sources
248
228
  this.metrics?.importPayload.bySource.inc({source: payloadInput.getPayloadEnvelopeSource().source});
249
229
  for (const {source} of payloadInput.getSampledColumnsWithSource()) {
250
230
  this.metrics?.importPayload.columnsBySource.inc({source});
251
231
  }
252
232
 
253
- // 10. Emit event after payload is fully verified and imported to fork choice, only for recent enough payloads
254
- if (this.clock.currentSlot - envelope.payload.slotNumber < EVENTSTREAM_EMIT_RECENT_EXECUTION_PAYLOAD_SLOTS) {
233
+ // 9. Emit event after payload is fully verified and imported to fork choice, only for recent enough payloads
234
+ if (this.clock.currentSlot - slot < EVENTSTREAM_EMIT_RECENT_EXECUTION_PAYLOAD_SLOTS) {
255
235
  this.emitter.emit(routes.events.EventType.executionPayload, {
256
- slot: envelope.payload.slotNumber,
236
+ slot,
257
237
  builderIndex: envelope.builderIndex,
258
238
  blockHash: blockHashHex,
259
239
  blockRoot: blockRootHex,
@@ -263,7 +243,7 @@ export async function importExecutionPayload(
263
243
  }
264
244
 
265
245
  this.logger.verbose("Execution payload imported", {
266
- slot: envelope.payload.slotNumber,
246
+ slot,
267
247
  builderIndex: envelope.builderIndex,
268
248
  blockRoot: blockRootHex,
269
249
  blockHash: blockHashHex,
@@ -89,7 +89,6 @@ export async function processBlocks(
89
89
  (block, i): FullyVerifiedBlock => ({
90
90
  blockInput: block,
91
91
  postState: postStates[i],
92
- postPayloadState: null,
93
92
  parentBlockSlot: parentSlots[i],
94
93
  executionStatus: executionStatuses[i],
95
94
  // start supporting optimistic syncing/processing
@@ -1,5 +1,5 @@
1
1
  import type {ChainForkConfig} from "@lodestar/config";
2
- import {BlockExecutionStatus, PayloadExecutionStatus} from "@lodestar/fork-choice";
2
+ import {BlockExecutionStatus} from "@lodestar/fork-choice";
3
3
  import {ForkSeq} from "@lodestar/params";
4
4
  import {DataAvailabilityStatus, IBeaconStateView, computeEpochAtSlot} from "@lodestar/state-transition";
5
5
  import type {IndexedAttestation, Slot, fulu} from "@lodestar/types";
@@ -43,8 +43,9 @@ export enum BlobSidecarValidation {
43
43
 
44
44
  export type ImportPayloadOpts = {
45
45
  /**
46
- * Set to true if envelope signature was already verified (e.g., during gossip/API validation).
47
- * When false/undefined, signature will be verified during import.
46
+ * Set to true when the envelope was already validated upstream (e.g., gossip/API validation):
47
+ * signature is trusted and execution_requests_root was already verified against the bid.
48
+ * When false/undefined, both are verified during import.
48
49
  */
49
50
  validSignature?: boolean;
50
51
  };
@@ -88,7 +89,14 @@ export type ImportBlockOpts = {
88
89
  seenTimestampSec?: number;
89
90
  };
90
91
 
91
- type FullyVerifiedBlockBase = {
92
+ /**
93
+ * A wrapper around a `SignedBeaconBlock` that indicates that this block is fully verified and ready to import.
94
+ *
95
+ * `executionStatus` reflects the outcome of execution payload verification at block-import time:
96
+ * - pre-gloas: Valid | Syncing | PreMerge (from EL notifyNewPayload against the in-block payload)
97
+ * - post-gloas: PayloadSeparated (payload arrives separately as an envelope and is imported later)
98
+ */
99
+ export type FullyVerifiedBlock = {
92
100
  blockInput: IBlockInput;
93
101
  postState: IBeaconStateView;
94
102
  parentBlockSlot: Slot;
@@ -98,25 +106,6 @@ type FullyVerifiedBlockBase = {
98
106
  indexedAttestations: IndexedAttestation[];
99
107
  /** Seen timestamp seconds */
100
108
  seenTimestampSec: number;
109
+ /** If the execution payload couldn't be verified because of EL syncing status, used in optimistic sync */
110
+ executionStatus: BlockExecutionStatus;
101
111
  };
102
-
103
- /**
104
- * A wrapper around a `SignedBeaconBlock` that indicates that this block is fully verified and ready to import.
105
- *
106
- * Discriminated union on `postPayloadState`:
107
- * - `null` → block has no pre-verified envelope; `executionStatus` is any `BlockExecutionStatus`
108
- * - non-null → envelope was pre-verified during state transition; `executionStatus` is narrowed to
109
- * `Valid | Syncing` (matching what `forkChoice.onExecutionPayload` expects)
110
- */
111
- export type FullyVerifiedBlock = FullyVerifiedBlockBase &
112
- (
113
- | {
114
- postPayloadState: null;
115
- /** If the execution payload couldn't be verified because of EL syncing status, used in optimistic sync or for merge block */
116
- executionStatus: BlockExecutionStatus;
117
- }
118
- | {
119
- postPayloadState: IBeaconStateView;
120
- executionStatus: PayloadExecutionStatus;
121
- }
122
- );
@@ -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 copy of latestBlockHeader to avoid mutating state.
36
+ const headerValue = {...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,17 @@ 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
+ const envelope = await this.getExecutionPayloadEnvelope(parentBlockSlot, parentBlockRootHex);
895
+ if (envelope === null) {
896
+ throw Error(`Parent execution payload envelope not found slot=${parentBlockSlot}, root=${parentBlockRootHex}`);
897
+ }
898
+ return envelope.message.executionRequests;
899
+ }
900
+
889
901
  async getDataColumnSidecars(blockSlot: Slot, blockRootHex: string): Promise<DataColumnSidecar[]> {
890
902
  const fork = this.config.getForkName(blockSlot);
891
903
 
@@ -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