@lodestar/beacon-node 1.43.0-dev.ade910fc78 → 1.43.0-dev.b741495bdc

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 (201) hide show
  1. package/lib/api/impl/beacon/blocks/index.d.ts.map +1 -1
  2. package/lib/api/impl/beacon/blocks/index.js +1 -4
  3. package/lib/api/impl/beacon/blocks/index.js.map +1 -1
  4. package/lib/api/impl/beacon/state/utils.d.ts +2 -2
  5. package/lib/api/impl/beacon/state/utils.d.ts.map +1 -1
  6. package/lib/api/impl/beacon/state/utils.js.map +1 -1
  7. package/lib/api/impl/validator/index.d.ts.map +1 -1
  8. package/lib/api/impl/validator/index.js +1 -4
  9. package/lib/api/impl/validator/index.js.map +1 -1
  10. package/lib/chain/GetBlobsTracker.d.ts +1 -1
  11. package/lib/chain/GetBlobsTracker.d.ts.map +1 -1
  12. package/lib/chain/GetBlobsTracker.js +1 -2
  13. package/lib/chain/GetBlobsTracker.js.map +1 -1
  14. package/lib/chain/archiveStore/archiveStore.d.ts.map +1 -1
  15. package/lib/chain/archiveStore/archiveStore.js.map +1 -1
  16. package/lib/chain/archiveStore/interface.d.ts +4 -4
  17. package/lib/chain/archiveStore/interface.d.ts.map +1 -1
  18. package/lib/chain/archiveStore/strategies/frequencyStateArchiveStrategy.d.ts +4 -4
  19. package/lib/chain/archiveStore/strategies/frequencyStateArchiveStrategy.d.ts.map +1 -1
  20. package/lib/chain/archiveStore/strategies/frequencyStateArchiveStrategy.js +2 -4
  21. package/lib/chain/archiveStore/strategies/frequencyStateArchiveStrategy.js.map +1 -1
  22. package/lib/chain/archiveStore/utils/archiveBlocks.d.ts +2 -2
  23. package/lib/chain/archiveStore/utils/archiveBlocks.d.ts.map +1 -1
  24. package/lib/chain/archiveStore/utils/archiveBlocks.js +110 -58
  25. package/lib/chain/archiveStore/utils/archiveBlocks.js.map +1 -1
  26. package/lib/chain/blocks/importBlock.d.ts.map +1 -1
  27. package/lib/chain/blocks/importBlock.js +23 -31
  28. package/lib/chain/blocks/importBlock.js.map +1 -1
  29. package/lib/chain/blocks/importExecutionPayload.d.ts +15 -14
  30. package/lib/chain/blocks/importExecutionPayload.d.ts.map +1 -1
  31. package/lib/chain/blocks/importExecutionPayload.js +63 -85
  32. package/lib/chain/blocks/importExecutionPayload.js.map +1 -1
  33. package/lib/chain/blocks/index.d.ts.map +1 -1
  34. package/lib/chain/blocks/index.js +1 -2
  35. package/lib/chain/blocks/index.js.map +1 -1
  36. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.d.ts +3 -0
  37. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.d.ts.map +1 -1
  38. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.js +20 -0
  39. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.js.map +1 -1
  40. package/lib/chain/blocks/payloadEnvelopeProcessor.d.ts +5 -0
  41. package/lib/chain/blocks/payloadEnvelopeProcessor.d.ts.map +1 -1
  42. package/lib/chain/blocks/payloadEnvelopeProcessor.js +6 -4
  43. package/lib/chain/blocks/payloadEnvelopeProcessor.js.map +1 -1
  44. package/lib/chain/blocks/types.d.ts +15 -21
  45. package/lib/chain/blocks/types.d.ts.map +1 -1
  46. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.d.ts +24 -0
  47. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.d.ts.map +1 -0
  48. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js +76 -0
  49. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js.map +1 -0
  50. package/lib/chain/blocks/verifyPayloadsDataAvailability.d.ts +14 -0
  51. package/lib/chain/blocks/verifyPayloadsDataAvailability.d.ts.map +1 -0
  52. package/lib/chain/blocks/verifyPayloadsDataAvailability.js +25 -0
  53. package/lib/chain/blocks/verifyPayloadsDataAvailability.js.map +1 -0
  54. package/lib/chain/blocks/writePayloadEnvelopeInputToDb.d.ts +1 -1
  55. package/lib/chain/blocks/writePayloadEnvelopeInputToDb.js +1 -1
  56. package/lib/chain/chain.d.ts +3 -3
  57. package/lib/chain/chain.d.ts.map +1 -1
  58. package/lib/chain/chain.js +16 -32
  59. package/lib/chain/chain.js.map +1 -1
  60. package/lib/chain/emitter.d.ts +16 -4
  61. package/lib/chain/emitter.d.ts.map +1 -1
  62. package/lib/chain/emitter.js +5 -0
  63. package/lib/chain/emitter.js.map +1 -1
  64. package/lib/chain/errors/attestationError.d.ts +8 -1
  65. package/lib/chain/errors/attestationError.d.ts.map +1 -1
  66. package/lib/chain/errors/attestationError.js +4 -0
  67. package/lib/chain/errors/attestationError.js.map +1 -1
  68. package/lib/chain/errors/executionPayloadBid.d.ts +5 -0
  69. package/lib/chain/errors/executionPayloadBid.d.ts.map +1 -1
  70. package/lib/chain/errors/executionPayloadBid.js +1 -0
  71. package/lib/chain/errors/executionPayloadBid.js.map +1 -1
  72. package/lib/chain/errors/executionPayloadEnvelope.d.ts +5 -0
  73. package/lib/chain/errors/executionPayloadEnvelope.d.ts.map +1 -1
  74. package/lib/chain/errors/executionPayloadEnvelope.js +1 -0
  75. package/lib/chain/errors/executionPayloadEnvelope.js.map +1 -1
  76. package/lib/chain/forkChoice/index.d.ts.map +1 -1
  77. package/lib/chain/forkChoice/index.js +11 -15
  78. package/lib/chain/forkChoice/index.js.map +1 -1
  79. package/lib/chain/interface.d.ts +2 -2
  80. package/lib/chain/interface.d.ts.map +1 -1
  81. package/lib/chain/prepareNextSlot.d.ts.map +1 -1
  82. package/lib/chain/prepareNextSlot.js +22 -16
  83. package/lib/chain/prepareNextSlot.js.map +1 -1
  84. package/lib/chain/produceBlock/computeNewStateRoot.d.ts +3 -9
  85. package/lib/chain/produceBlock/computeNewStateRoot.d.ts.map +1 -1
  86. package/lib/chain/produceBlock/computeNewStateRoot.js +5 -32
  87. package/lib/chain/produceBlock/computeNewStateRoot.js.map +1 -1
  88. package/lib/chain/produceBlock/produceBlockBody.d.ts +3 -8
  89. package/lib/chain/produceBlock/produceBlockBody.d.ts.map +1 -1
  90. package/lib/chain/produceBlock/produceBlockBody.js +30 -19
  91. package/lib/chain/produceBlock/produceBlockBody.js.map +1 -1
  92. package/lib/chain/regen/errors.d.ts +1 -11
  93. package/lib/chain/regen/errors.d.ts.map +1 -1
  94. package/lib/chain/regen/errors.js +0 -2
  95. package/lib/chain/regen/errors.js.map +1 -1
  96. package/lib/chain/regen/interface.d.ts +6 -11
  97. package/lib/chain/regen/interface.d.ts.map +1 -1
  98. package/lib/chain/regen/queued.d.ts +6 -10
  99. package/lib/chain/regen/queued.d.ts.map +1 -1
  100. package/lib/chain/regen/queued.js +3 -10
  101. package/lib/chain/regen/queued.js.map +1 -1
  102. package/lib/chain/regen/regen.d.ts +0 -5
  103. package/lib/chain/regen/regen.d.ts.map +1 -1
  104. package/lib/chain/regen/regen.js +0 -8
  105. package/lib/chain/regen/regen.js.map +1 -1
  106. package/lib/chain/stateCache/persistentCheckpointsCache.d.ts +1 -7
  107. package/lib/chain/stateCache/persistentCheckpointsCache.d.ts.map +1 -1
  108. package/lib/chain/stateCache/persistentCheckpointsCache.js +4 -9
  109. package/lib/chain/stateCache/persistentCheckpointsCache.js.map +1 -1
  110. package/lib/chain/stateCache/types.d.ts +0 -6
  111. package/lib/chain/stateCache/types.d.ts.map +1 -1
  112. package/lib/chain/stateCache/types.js.map +1 -1
  113. package/lib/chain/validation/aggregateAndProof.js +12 -0
  114. package/lib/chain/validation/aggregateAndProof.js.map +1 -1
  115. package/lib/chain/validation/attestation.d.ts.map +1 -1
  116. package/lib/chain/validation/attestation.js +12 -0
  117. package/lib/chain/validation/attestation.js.map +1 -1
  118. package/lib/chain/validation/executionPayloadBid.d.ts.map +1 -1
  119. package/lib/chain/validation/executionPayloadBid.js +13 -1
  120. package/lib/chain/validation/executionPayloadBid.js.map +1 -1
  121. package/lib/chain/validation/executionPayloadEnvelope.d.ts.map +1 -1
  122. package/lib/chain/validation/executionPayloadEnvelope.js +21 -11
  123. package/lib/chain/validation/executionPayloadEnvelope.js.map +1 -1
  124. package/lib/chain/validation/payloadAttestationMessage.d.ts.map +1 -1
  125. package/lib/chain/validation/payloadAttestationMessage.js +4 -3
  126. package/lib/chain/validation/payloadAttestationMessage.js.map +1 -1
  127. package/lib/db/repositories/executionPayloadEnvelopeArchive.js +1 -1
  128. package/lib/db/repositories/executionPayloadEnvelopeArchive.js.map +1 -1
  129. package/lib/execution/engine/http.d.ts.map +1 -1
  130. package/lib/execution/engine/http.js +21 -14
  131. package/lib/execution/engine/http.js.map +1 -1
  132. package/lib/execution/engine/interface.d.ts +1 -0
  133. package/lib/execution/engine/interface.d.ts.map +1 -1
  134. package/lib/execution/engine/mock.d.ts.map +1 -1
  135. package/lib/execution/engine/mock.js +6 -0
  136. package/lib/execution/engine/mock.js.map +1 -1
  137. package/lib/execution/engine/types.d.ts +20 -0
  138. package/lib/execution/engine/types.d.ts.map +1 -1
  139. package/lib/execution/engine/types.js +18 -0
  140. package/lib/execution/engine/types.js.map +1 -1
  141. package/lib/network/gossip/topic.d.ts +3 -2
  142. package/lib/network/gossip/topic.d.ts.map +1 -1
  143. package/lib/network/network.js +1 -1
  144. package/lib/network/network.js.map +1 -1
  145. package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
  146. package/lib/network/processor/gossipHandlers.js +22 -6
  147. package/lib/network/processor/gossipHandlers.js.map +1 -1
  148. package/lib/node/nodejs.d.ts.map +1 -1
  149. package/lib/node/nodejs.js +4 -2
  150. package/lib/node/nodejs.js.map +1 -1
  151. package/lib/util/sszBytes.d.ts.map +1 -1
  152. package/lib/util/sszBytes.js +16 -3
  153. package/lib/util/sszBytes.js.map +1 -1
  154. package/package.json +16 -16
  155. package/src/api/impl/beacon/blocks/index.ts +1 -4
  156. package/src/api/impl/beacon/state/utils.ts +2 -2
  157. package/src/api/impl/validator/index.ts +3 -6
  158. package/src/chain/GetBlobsTracker.ts +1 -2
  159. package/src/chain/archiveStore/archiveStore.ts +5 -5
  160. package/src/chain/archiveStore/interface.ts +4 -4
  161. package/src/chain/archiveStore/strategies/frequencyStateArchiveStrategy.ts +6 -8
  162. package/src/chain/archiveStore/utils/archiveBlocks.ts +153 -94
  163. package/src/chain/blocks/importBlock.ts +22 -35
  164. package/src/chain/blocks/importExecutionPayload.ts +77 -103
  165. package/src/chain/blocks/index.ts +1 -2
  166. package/src/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.ts +27 -0
  167. package/src/chain/blocks/payloadEnvelopeProcessor.ts +6 -5
  168. package/src/chain/blocks/types.ts +15 -26
  169. package/src/chain/blocks/verifyExecutionPayloadEnvelope.ts +129 -0
  170. package/src/chain/blocks/verifyPayloadsDataAvailability.ts +38 -0
  171. package/src/chain/blocks/writePayloadEnvelopeInputToDb.ts +1 -1
  172. package/src/chain/chain.ts +23 -48
  173. package/src/chain/emitter.ts +15 -3
  174. package/src/chain/errors/attestationError.ts +6 -1
  175. package/src/chain/errors/executionPayloadBid.ts +6 -0
  176. package/src/chain/errors/executionPayloadEnvelope.ts +6 -0
  177. package/src/chain/forkChoice/index.ts +8 -20
  178. package/src/chain/interface.ts +2 -2
  179. package/src/chain/prepareNextSlot.ts +25 -16
  180. package/src/chain/produceBlock/computeNewStateRoot.ts +6 -43
  181. package/src/chain/produceBlock/produceBlockBody.ts +41 -20
  182. package/src/chain/regen/errors.ts +1 -6
  183. package/src/chain/regen/interface.ts +6 -11
  184. package/src/chain/regen/queued.ts +6 -14
  185. package/src/chain/regen/regen.ts +0 -8
  186. package/src/chain/stateCache/persistentCheckpointsCache.ts +5 -15
  187. package/src/chain/stateCache/types.ts +0 -3
  188. package/src/chain/validation/aggregateAndProof.ts +13 -0
  189. package/src/chain/validation/attestation.ts +13 -0
  190. package/src/chain/validation/executionPayloadBid.ts +14 -0
  191. package/src/chain/validation/executionPayloadEnvelope.ts +22 -12
  192. package/src/chain/validation/payloadAttestationMessage.ts +5 -3
  193. package/src/db/repositories/executionPayloadEnvelopeArchive.ts +1 -1
  194. package/src/execution/engine/http.ts +21 -14
  195. package/src/execution/engine/interface.ts +1 -0
  196. package/src/execution/engine/mock.ts +8 -1
  197. package/src/execution/engine/types.ts +41 -0
  198. package/src/network/network.ts +1 -1
  199. package/src/network/processor/gossipHandlers.ts +26 -10
  200. package/src/node/nodejs.ts +4 -2
  201. package/src/util/sszBytes.ts +21 -3
@@ -1,14 +1,18 @@
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 {byteArrayEquals, 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";
15
+ import {verifyPayloadsDataAvailability} from "./verifyPayloadsDataAvailability.js";
12
16
 
13
17
  const EVENTSTREAM_EMIT_RECENT_EXECUTION_PAYLOAD_SLOTS = 64;
14
18
 
@@ -16,7 +20,7 @@ export enum PayloadErrorCode {
16
20
  EXECUTION_ENGINE_INVALID = "PAYLOAD_ERROR_EXECUTION_ENGINE_INVALID",
17
21
  EXECUTION_ENGINE_ERROR = "PAYLOAD_ERROR_EXECUTION_ENGINE_ERROR",
18
22
  BLOCK_NOT_IN_FORK_CHOICE = "PAYLOAD_ERROR_BLOCK_NOT_IN_FORK_CHOICE",
19
- STATE_TRANSITION_ERROR = "PAYLOAD_ERROR_STATE_TRANSITION_ERROR",
23
+ ENVELOPE_VERIFICATION_ERROR = "PAYLOAD_ERROR_ENVELOPE_VERIFICATION_ERROR",
20
24
  INVALID_SIGNATURE = "PAYLOAD_ERROR_INVALID_SIGNATURE",
21
25
  }
22
26
 
@@ -36,7 +40,7 @@ export type PayloadErrorType =
36
40
  blockRootHex: string;
37
41
  }
38
42
  | {
39
- code: PayloadErrorCode.STATE_TRANSITION_ERROR;
43
+ code: PayloadErrorCode.ENVELOPE_VERIFICATION_ERROR;
40
44
  message: string;
41
45
  }
42
46
  | {
@@ -68,37 +72,40 @@ function toForkChoiceExecutionStatus(status: ExecutionPayloadStatus): PayloadExe
68
72
  /**
69
73
  * Import an execution payload envelope after all data is available.
70
74
  *
71
- * This function:
72
- * 1. Emits `execution_payload_available` if payload is for current slot
73
- * 2. Gets the ProtoBlock from fork choice
74
- * 3. Applies write-queue backpressure (waitForSpace) early, before verification
75
- * 4. Regenerates the block state
76
- * 5. Runs EL verification (notifyNewPayload) in parallel with signature verification and processExecutionPayloadEnvelope
77
- * 6. Persists verified payload envelope to hot DB
78
- * 7. Updates fork choice
79
- * 8. Caches the post-execution payload state
80
- * 9. Records metrics for column sources
81
- * 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.
82
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
83
88
  */
84
89
  export async function importExecutionPayload(
85
90
  this: BeaconChain,
86
91
  payloadInput: PayloadEnvelopeInput,
92
+ signal: AbortSignal,
87
93
  opts: ImportPayloadOpts = {}
88
94
  ): Promise<void> {
89
95
  const signedEnvelope = payloadInput.getPayloadEnvelope();
90
96
  const envelope = signedEnvelope.message;
97
+ const slot = envelope.payload.slotNumber;
91
98
  const blockRootHex = payloadInput.blockRootHex;
92
99
  const blockHashHex = payloadInput.getBlockHashHex();
93
- const fork = this.config.getForkName(envelope.slot);
100
+ const fork = this.config.getForkName(slot);
94
101
 
95
- // 1. Emit `execution_payload_available` event at the start of import. At this point the payload input
96
- // is already complete, so the payload and required data are available for payload attestation.
97
- // This event is only about availability, not validity of the execution payload, hence we can emit
98
- // it before getting a response from the execution client on whether the payload is valid or not.
99
- if (this.clock.currentSlot - envelope.slot < 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) {
100
107
  this.emitter.emit(routes.events.EventType.executionPayloadAvailable, {
101
- slot: envelope.slot,
108
+ slot,
102
109
  blockRoot: blockRootHex,
103
110
  });
104
111
  }
@@ -112,12 +119,11 @@ export async function importExecutionPayload(
112
119
  });
113
120
  }
114
121
 
115
- // 3. Apply backpressure from the write queue early, before doing verification work.
116
- // The actual DB write is deferred until after verification succeeds.
117
- await this.unfinalizedPayloadEnvelopeWrites.waitForSpace();
122
+ // 3. Wait for data columns to be available.
123
+ // The helper is shared with future gloas sync services; take the single-item batch form here.
124
+ await verifyPayloadsDataAvailability([payloadInput], signal);
118
125
 
119
- // 4. Get pre-state for processExecutionPayloadEnvelope
120
- // We need the block state (post-block, pre-payload) to process the envelope
126
+ // 4. Regenerate state for envelope verification
121
127
  const blockState = await this.regen.getBlockSlotState(
122
128
  protoBlock,
123
129
  protoBlock.slot,
@@ -126,15 +132,30 @@ export async function importExecutionPayload(
126
132
  );
127
133
  if (!isStatePostGloas(blockState)) {
128
134
  throw new PayloadError({
129
- code: PayloadErrorCode.STATE_TRANSITION_ERROR,
130
- 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,
131
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
+ );
132
155
  }
133
156
 
134
- // 5. Run verification steps in parallel
135
- // Note: No data availability check needed here - importExecutionPayload is only
136
- // called when payloadInput.isComplete() is true, so all data is already available.
137
- const [execResult, signatureValid, postPayloadResult] = await Promise.all([
157
+ // 5a. Run EL and signature verification in parallel
158
+ const [execResult, signatureValid] = await Promise.all([
138
159
  this.executionEngine.notifyNewPayload(
139
160
  fork,
140
161
  envelope.payload,
@@ -145,45 +166,22 @@ export async function importExecutionPayload(
145
166
 
146
167
  opts.validSignature === true
147
168
  ? Promise.resolve(true)
148
- : (async () => {
149
- const signatureSet = getExecutionPayloadEnvelopeSignatureSet(
150
- this.config,
151
- this.pubkeyCache,
152
- blockState,
153
- signedEnvelope,
154
- payloadInput.proposerIndex
155
- );
156
- return this.bls.verifySignatureSets([signatureSet]);
157
- })(),
158
-
159
- // Signature verified separately above.
160
- // State root check is done separately below with better error typing (matching block pipeline pattern).
161
- (async () => {
162
- try {
163
- return {
164
- postPayloadState: blockState.processExecutionPayloadEnvelope(signedEnvelope, {
165
- verifySignature: false,
166
- verifyStateRoot: false,
167
- }),
168
- };
169
- } catch (e) {
170
- throw new PayloadError(
171
- {
172
- code: PayloadErrorCode.STATE_TRANSITION_ERROR,
173
- message: (e as Error).message,
174
- },
175
- `State transition error: ${(e as Error).message}`
176
- );
177
- }
178
- })(),
169
+ : verifyExecutionPayloadEnvelopeSignature(
170
+ this.config,
171
+ blockState,
172
+ this.pubkeyCache,
173
+ signedEnvelope,
174
+ payloadInput.proposerIndex,
175
+ this.bls
176
+ ),
179
177
  ]);
180
178
 
181
- // 5a. Check signature verification result
179
+ // 5b. Check signature verification result
182
180
  if (!signatureValid) {
183
181
  throw new PayloadError({code: PayloadErrorCode.INVALID_SIGNATURE});
184
182
  }
185
183
 
186
- // 5b. Handle EL response
184
+ // 5c. Handle EL response
187
185
  switch (execResult.status) {
188
186
  case ExecutionPayloadStatus.VALID:
189
187
  break;
@@ -209,69 +207,45 @@ export async function importExecutionPayload(
209
207
  });
210
208
  }
211
209
 
212
- // 5c. Verify envelope state root matches post-state
213
- const postPayloadState = postPayloadResult.postPayloadState;
214
- const postPayloadStateRoot = postPayloadState.hashTreeRoot();
215
- if (!byteArrayEquals(envelope.stateRoot, postPayloadStateRoot)) {
216
- throw new PayloadError({
217
- code: PayloadErrorCode.STATE_TRANSITION_ERROR,
218
- message: `Envelope state root mismatch expected=${toRootHex(envelope.stateRoot)} actual=${toRootHex(postPayloadStateRoot)}`,
219
- });
220
- }
221
-
222
- // 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();
223
213
  this.unfinalizedPayloadEnvelopeWrites.push(payloadInput).catch((e) => {
224
214
  if (!isQueueErrorAborted(e)) {
225
215
  this.logger.error(
226
216
  "Error pushing payload envelope to unfinalized write queue",
227
- {slot: envelope.slot, blockRoot: blockRootHex},
217
+ {slot, blockRoot: blockRootHex},
228
218
  e as Error
229
219
  );
230
220
  }
231
221
  });
232
222
 
233
- // 7. Update fork choice
234
- this.forkChoice.onExecutionPayload(
235
- blockRootHex,
236
- blockHashHex,
237
- envelope.payload.blockNumber,
238
- toRootHex(postPayloadStateRoot),
239
- toForkChoiceExecutionStatus(execResult.status)
240
- );
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);
241
226
 
242
- // 8. Cache payload state
243
- this.regen.processPayloadState(postPayloadState);
244
- if (postPayloadState.slot % SLOTS_PER_EPOCH === 0) {
245
- const {checkpoint} = postPayloadState.computeAnchorCheckpoint();
246
- this.regen.addCheckpointState(checkpoint, postPayloadState, true);
247
- }
248
-
249
- // 9. Record metrics for payload envelope and column sources
227
+ // 8. Record metrics for payload envelope and column sources
250
228
  this.metrics?.importPayload.bySource.inc({source: payloadInput.getPayloadEnvelopeSource().source});
251
229
  for (const {source} of payloadInput.getSampledColumnsWithSource()) {
252
230
  this.metrics?.importPayload.columnsBySource.inc({source});
253
231
  }
254
232
 
255
- const stateRootHex = toRootHex(envelope.stateRoot);
256
-
257
- // 10. Emit event after payload is fully verified and imported to fork choice, only for recent enough payloads
258
- if (this.clock.currentSlot - envelope.slot < 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) {
259
235
  this.emitter.emit(routes.events.EventType.executionPayload, {
260
- slot: envelope.slot,
236
+ slot,
261
237
  builderIndex: envelope.builderIndex,
262
238
  blockHash: blockHashHex,
263
239
  blockRoot: blockRootHex,
264
- stateRoot: stateRootHex,
265
240
  // TODO GLOAS: revisit once we support optimistic import
266
241
  executionOptimistic: false,
267
242
  });
268
243
  }
269
244
 
270
245
  this.logger.verbose("Execution payload imported", {
271
- slot: envelope.slot,
246
+ slot,
272
247
  builderIndex: envelope.builderIndex,
273
248
  blockRoot: blockRootHex,
274
249
  blockHash: blockHashHex,
275
- stateRoot: stateRootHex,
276
250
  });
277
251
  }
@@ -88,8 +88,7 @@ export async function processBlocks(
88
88
  const fullyVerifiedBlocks = relevantBlocks.map(
89
89
  (block, i): FullyVerifiedBlock => ({
90
90
  blockInput: block,
91
- postBlockState: postStates[i],
92
- postPayloadState: null,
91
+ postState: postStates[i],
93
92
  parentBlockSlot: parentSlots[i],
94
93
  executionStatus: executionStatuses[i],
95
94
  // start supporting optimistic syncing/processing
@@ -73,6 +73,7 @@ export class PayloadEnvelopeInput {
73
73
  private timeCreatedSec: number;
74
74
 
75
75
  private readonly payloadEnvelopeDataPromise: PromiseParts<gloas.SignedExecutionPayloadEnvelope>;
76
+ private readonly allDataPromise: PromiseParts<gloas.DataColumnSidecar[]>;
76
77
  private readonly columnsDataPromise: PromiseParts<gloas.DataColumnSidecar[]>;
77
78
 
78
79
  state: PayloadEnvelopeInputState;
@@ -97,6 +98,7 @@ export class PayloadEnvelopeInput {
97
98
  this.custodyColumns = props.custodyColumns;
98
99
  this.timeCreatedSec = props.timeCreatedSec;
99
100
  this.payloadEnvelopeDataPromise = createPromise();
101
+ this.allDataPromise = createPromise();
100
102
  this.columnsDataPromise = createPromise();
101
103
 
102
104
  const noBlobs = props.bid.blobKzgCommitments.length === 0;
@@ -105,6 +107,7 @@ export class PayloadEnvelopeInput {
105
107
 
106
108
  if (hasAllData) {
107
109
  this.state = {hasPayload: false, hasAllData: true, hasComputedAllData: true};
110
+ this.allDataPromise.resolve(this.getSampledColumns());
108
111
  this.columnsDataPromise.resolve(this.getSampledColumns());
109
112
  } else {
110
113
  this.state = {hasPayload: false, hasAllData: false, hasComputedAllData: false};
@@ -203,6 +206,12 @@ export class PayloadEnvelopeInput {
203
206
  return true;
204
207
  }
205
208
 
209
+ // Resolve allDataPromise on the first transition to hasAllData (either sampled-complete or
210
+ // reconstruction-threshold branch). Guarded so it fires exactly once.
211
+ if (!this.state.hasAllData && hasAllData) {
212
+ this.allDataPromise.resolve(sampledColumns);
213
+ }
214
+
206
215
  if (hasComputedAllData) {
207
216
  this.columnsDataPromise.resolve(sampledColumns);
208
217
  }
@@ -315,6 +324,24 @@ export class PayloadEnvelopeInput {
315
324
  return this.state.hasComputedAllData;
316
325
  }
317
326
 
327
+ waitForAllData(timeout: number, signal?: AbortSignal): Promise<gloas.DataColumnSidecar[]> {
328
+ if (this.state.hasAllData) {
329
+ return Promise.resolve(this.getSampledColumns());
330
+ }
331
+ return withTimeout(() => this.allDataPromise.promise, timeout, signal);
332
+ }
333
+
334
+ async waitForEnvelopeAndAllData(timeout: number, signal?: AbortSignal): Promise<this> {
335
+ if (!this.state.hasPayload || !this.state.hasAllData) {
336
+ await withTimeout(
337
+ () => Promise.all([this.payloadEnvelopeDataPromise.promise, this.allDataPromise.promise]),
338
+ timeout,
339
+ signal
340
+ );
341
+ }
342
+ return this;
343
+ }
344
+
318
345
  waitForComputedAllData(timeout: number, signal?: AbortSignal): Promise<gloas.DataColumnSidecar[]> {
319
346
  if (this.state.hasComputedAllData) {
320
347
  return Promise.resolve(this.getSampledColumns());
@@ -16,6 +16,11 @@ enum PayloadEnvelopeImportStatus {
16
16
 
17
17
  /**
18
18
  * PayloadEnvelopeProcessor processes payload envelope jobs in a queued fashion, one after the other.
19
+ *
20
+ * Jobs are enqueued only on envelope arrival (gossip or API). The envelope may reach us before
21
+ * the sampled data columns; importExecutionPayload awaits `verifyPayloadsDataAvailability`
22
+ * internally, so a queued job can pend for up to `PAYLOAD_DATA_AVAILABILITY_TIMEOUT` while
23
+ * waiting for columns. Duplicate triggers for the same payloadInput are deduped via `importStatus`.
19
24
  */
20
25
  export class PayloadEnvelopeProcessor {
21
26
  readonly jobQueue: JobItemQueue<[PayloadEnvelopeInput, ImportPayloadOpts], void>;
@@ -25,7 +30,7 @@ export class PayloadEnvelopeProcessor {
25
30
  this.jobQueue = new JobItemQueue<[PayloadEnvelopeInput, ImportPayloadOpts], void>(
26
31
  (payloadInput, opts) => {
27
32
  this.importStatus.set(payloadInput, PayloadEnvelopeImportStatus.importing);
28
- return importExecutionPayload.call(chain, payloadInput, opts);
33
+ return importExecutionPayload.call(chain, payloadInput, signal, opts);
29
34
  },
30
35
  {maxLength: QUEUE_MAX_LENGTH, noYieldIfOneItem: true, signal},
31
36
  metrics?.payloadEnvelopeProcessorQueue ?? undefined
@@ -33,10 +38,6 @@ export class PayloadEnvelopeProcessor {
33
38
  }
34
39
 
35
40
  async processPayloadEnvelopeJob(payloadInput: PayloadEnvelopeInput, opts: ImportPayloadOpts = {}): Promise<void> {
36
- if (!payloadInput.isComplete()) {
37
- return;
38
- }
39
-
40
41
  if (this.importStatus.get(payloadInput) !== undefined) {
41
42
  return;
42
43
  }
@@ -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,9 +89,16 @@ 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
- postBlockState: IBeaconStateView;
101
+ postState: IBeaconStateView;
94
102
  parentBlockSlot: Slot;
95
103
  proposerBalanceDelta: number;
96
104
  dataAvailabilityStatus: DataAvailabilityStatus;
@@ -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
+ }
@@ -0,0 +1,38 @@
1
+ import {DataAvailabilityStatus} from "@lodestar/state-transition";
2
+ import {gloas} from "@lodestar/types";
3
+ import {PayloadEnvelopeInput} from "../seenCache/seenPayloadEnvelopeInput.js";
4
+
5
+ // we can now wait for full 12 seconds because sync and reconstruction will try pulling
6
+ // the data columns from the network anyway while the envelope is being processed
7
+ export const PAYLOAD_DATA_AVAILABILITY_TIMEOUT = 12_000;
8
+
9
+ /**
10
+ * Verifies that all payload envelope inputs have their data columns available.
11
+ * - Waits a max of PAYLOAD_DATA_AVAILABILITY_TIMEOUT for all data to be available
12
+ * - Returns the time at which all data was available
13
+ * - Returns the data availability status for each payload input
14
+ */
15
+ export async function verifyPayloadsDataAvailability(
16
+ payloadInputs: PayloadEnvelopeInput[],
17
+ signal: AbortSignal
18
+ ): Promise<{
19
+ dataAvailabilityStatuses: DataAvailabilityStatus[];
20
+ availableTime: number;
21
+ }> {
22
+ const promises: Promise<gloas.DataColumnSidecar[]>[] = [];
23
+ for (const payloadInput of payloadInputs) {
24
+ if (!payloadInput.hasAllData()) {
25
+ promises.push(payloadInput.waitForAllData(PAYLOAD_DATA_AVAILABILITY_TIMEOUT, signal));
26
+ }
27
+ }
28
+ await Promise.all(promises);
29
+
30
+ const availableTime = Math.max(0, Math.max(...payloadInputs.map((payloadInput) => payloadInput.getTimeComplete())));
31
+ const dataAvailabilityStatuses: DataAvailabilityStatus[] = payloadInputs.map((payloadInput) =>
32
+ payloadInput.getBlobKzgCommitments().length === 0
33
+ ? DataAvailabilityStatus.NotRequired
34
+ : DataAvailabilityStatus.Available
35
+ );
36
+
37
+ return {dataAvailabilityStatuses, availableTime};
38
+ }
@@ -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
  */