@lodestar/beacon-node 1.43.0-rc.5 → 1.44.0-dev.00facd40a8

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 (164) hide show
  1. package/lib/api/impl/beacon/blocks/index.d.ts.map +1 -1
  2. package/lib/api/impl/beacon/blocks/index.js +30 -0
  3. package/lib/api/impl/beacon/blocks/index.js.map +1 -1
  4. package/lib/api/impl/beacon/pool/index.d.ts.map +1 -1
  5. package/lib/api/impl/beacon/pool/index.js +46 -5
  6. package/lib/api/impl/beacon/pool/index.js.map +1 -1
  7. package/lib/api/impl/debug/index.d.ts.map +1 -1
  8. package/lib/api/impl/debug/index.js +69 -12
  9. package/lib/api/impl/debug/index.js.map +1 -1
  10. package/lib/api/impl/validator/index.d.ts.map +1 -1
  11. package/lib/api/impl/validator/index.js +119 -53
  12. package/lib/api/impl/validator/index.js.map +1 -1
  13. package/lib/chain/blocks/importBlock.d.ts.map +1 -1
  14. package/lib/chain/blocks/importBlock.js +5 -2
  15. package/lib/chain/blocks/importBlock.js.map +1 -1
  16. package/lib/chain/blocks/importExecutionPayload.d.ts.map +1 -1
  17. package/lib/chain/blocks/importExecutionPayload.js +5 -3
  18. package/lib/chain/blocks/importExecutionPayload.js.map +1 -1
  19. package/lib/chain/chain.d.ts +3 -2
  20. package/lib/chain/chain.d.ts.map +1 -1
  21. package/lib/chain/chain.js +5 -2
  22. package/lib/chain/chain.js.map +1 -1
  23. package/lib/chain/errors/executionPayloadBid.d.ts +29 -1
  24. package/lib/chain/errors/executionPayloadBid.d.ts.map +1 -1
  25. package/lib/chain/errors/executionPayloadBid.js +5 -0
  26. package/lib/chain/errors/executionPayloadBid.js.map +1 -1
  27. package/lib/chain/errors/payloadAttestation.d.ts +6 -0
  28. package/lib/chain/errors/payloadAttestation.d.ts.map +1 -1
  29. package/lib/chain/errors/payloadAttestation.js +1 -0
  30. package/lib/chain/errors/payloadAttestation.js.map +1 -1
  31. package/lib/chain/forkChoice/index.d.ts.map +1 -1
  32. package/lib/chain/forkChoice/index.js +14 -4
  33. package/lib/chain/forkChoice/index.js.map +1 -1
  34. package/lib/chain/interface.d.ts +2 -1
  35. package/lib/chain/interface.d.ts.map +1 -1
  36. package/lib/chain/interface.js.map +1 -1
  37. package/lib/chain/lightClient/index.d.ts.map +1 -1
  38. package/lib/chain/lightClient/index.js +1 -1
  39. package/lib/chain/lightClient/index.js.map +1 -1
  40. package/lib/chain/opPools/executionPayloadBidPool.d.ts +4 -4
  41. package/lib/chain/opPools/executionPayloadBidPool.d.ts.map +1 -1
  42. package/lib/chain/opPools/executionPayloadBidPool.js +6 -4
  43. package/lib/chain/opPools/executionPayloadBidPool.js.map +1 -1
  44. package/lib/chain/opPools/index.d.ts +1 -0
  45. package/lib/chain/opPools/index.d.ts.map +1 -1
  46. package/lib/chain/opPools/index.js +1 -0
  47. package/lib/chain/opPools/index.js.map +1 -1
  48. package/lib/chain/opPools/payloadAttestationPool.d.ts +1 -1
  49. package/lib/chain/opPools/payloadAttestationPool.d.ts.map +1 -1
  50. package/lib/chain/opPools/payloadAttestationPool.js +30 -10
  51. package/lib/chain/opPools/payloadAttestationPool.js.map +1 -1
  52. package/lib/chain/opPools/proposerPreferencesPool.d.ts +29 -0
  53. package/lib/chain/opPools/proposerPreferencesPool.d.ts.map +1 -0
  54. package/lib/chain/opPools/proposerPreferencesPool.js +56 -0
  55. package/lib/chain/opPools/proposerPreferencesPool.js.map +1 -0
  56. package/lib/chain/prepareNextSlot.d.ts.map +1 -1
  57. package/lib/chain/prepareNextSlot.js +2 -1
  58. package/lib/chain/prepareNextSlot.js.map +1 -1
  59. package/lib/chain/produceBlock/produceBlockBody.d.ts +7 -1
  60. package/lib/chain/produceBlock/produceBlockBody.d.ts.map +1 -1
  61. package/lib/chain/produceBlock/produceBlockBody.js +107 -18
  62. package/lib/chain/produceBlock/produceBlockBody.js.map +1 -1
  63. package/lib/chain/regen/interface.d.ts +2 -0
  64. package/lib/chain/regen/interface.d.ts.map +1 -1
  65. package/lib/chain/regen/interface.js +2 -0
  66. package/lib/chain/regen/interface.js.map +1 -1
  67. package/lib/chain/validation/executionPayloadBid.d.ts +7 -3
  68. package/lib/chain/validation/executionPayloadBid.d.ts.map +1 -1
  69. package/lib/chain/validation/executionPayloadBid.js +119 -28
  70. package/lib/chain/validation/executionPayloadBid.js.map +1 -1
  71. package/lib/chain/validation/payloadAttestationMessage.d.ts +1 -1
  72. package/lib/chain/validation/payloadAttestationMessage.d.ts.map +1 -1
  73. package/lib/chain/validation/payloadAttestationMessage.js +29 -7
  74. package/lib/chain/validation/payloadAttestationMessage.js.map +1 -1
  75. package/lib/chain/validatorMonitor.d.ts +1 -0
  76. package/lib/chain/validatorMonitor.d.ts.map +1 -1
  77. package/lib/chain/validatorMonitor.js +16 -0
  78. package/lib/chain/validatorMonitor.js.map +1 -1
  79. package/lib/execution/builder/index.d.ts +1 -2
  80. package/lib/execution/builder/index.d.ts.map +1 -1
  81. package/lib/execution/builder/index.js +0 -1
  82. package/lib/execution/builder/index.js.map +1 -1
  83. package/lib/execution/engine/interface.d.ts +1 -0
  84. package/lib/execution/engine/interface.d.ts.map +1 -1
  85. package/lib/execution/engine/types.d.ts +2 -0
  86. package/lib/execution/engine/types.d.ts.map +1 -1
  87. package/lib/execution/engine/types.js +2 -0
  88. package/lib/execution/engine/types.js.map +1 -1
  89. package/lib/metrics/metrics/lodestar.d.ts +1 -1
  90. package/lib/metrics/metrics/lodestar.d.ts.map +1 -1
  91. package/lib/metrics/metrics/lodestar.js +4 -3
  92. package/lib/metrics/metrics/lodestar.js.map +1 -1
  93. package/lib/network/gossip/topic.d.ts +1 -1
  94. package/lib/network/interface.d.ts +2 -0
  95. package/lib/network/interface.d.ts.map +1 -1
  96. package/lib/network/network.d.ts +2 -0
  97. package/lib/network/network.d.ts.map +1 -1
  98. package/lib/network/network.js +10 -0
  99. package/lib/network/network.js.map +1 -1
  100. package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
  101. package/lib/network/processor/gossipHandlers.js +19 -6
  102. package/lib/network/processor/gossipHandlers.js.map +1 -1
  103. package/lib/network/reqresp/handlers/dataColumnSidecarsByRange.d.ts.map +1 -1
  104. package/lib/network/reqresp/handlers/dataColumnSidecarsByRange.js +5 -0
  105. package/lib/network/reqresp/handlers/dataColumnSidecarsByRange.js.map +1 -1
  106. package/lib/network/reqresp/handlers/dataColumnSidecarsByRoot.js +1 -1
  107. package/lib/network/reqresp/handlers/dataColumnSidecarsByRoot.js.map +1 -1
  108. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRange.js +3 -3
  109. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRange.js.map +1 -1
  110. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRoot.d.ts +2 -1
  111. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRoot.d.ts.map +1 -1
  112. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRoot.js +15 -1
  113. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRoot.js.map +1 -1
  114. package/lib/network/reqresp/handlers/index.js +2 -2
  115. package/lib/network/reqresp/handlers/index.js.map +1 -1
  116. package/lib/network/reqresp/utils/dataColumnResponseValidation.d.ts.map +1 -1
  117. package/lib/network/reqresp/utils/dataColumnResponseValidation.js +8 -0
  118. package/lib/network/reqresp/utils/dataColumnResponseValidation.js.map +1 -1
  119. package/lib/util/dependentRoot.d.ts +6 -2
  120. package/lib/util/dependentRoot.d.ts.map +1 -1
  121. package/lib/util/dependentRoot.js +20 -16
  122. package/lib/util/dependentRoot.js.map +1 -1
  123. package/package.json +14 -15
  124. package/src/api/impl/beacon/blocks/index.ts +36 -0
  125. package/src/api/impl/beacon/pool/index.ts +59 -4
  126. package/src/api/impl/debug/index.ts +73 -12
  127. package/src/api/impl/validator/index.ts +131 -54
  128. package/src/chain/blocks/importBlock.ts +10 -2
  129. package/src/chain/blocks/importExecutionPayload.ts +8 -2
  130. package/src/chain/chain.ts +5 -0
  131. package/src/chain/errors/executionPayloadBid.ts +27 -1
  132. package/src/chain/errors/payloadAttestation.ts +2 -0
  133. package/src/chain/forkChoice/index.ts +14 -4
  134. package/src/chain/interface.ts +2 -0
  135. package/src/chain/lightClient/index.ts +6 -6
  136. package/src/chain/opPools/executionPayloadBidPool.ts +10 -9
  137. package/src/chain/opPools/index.ts +1 -0
  138. package/src/chain/opPools/payloadAttestationPool.ts +34 -10
  139. package/src/chain/opPools/proposerPreferencesPool.ts +59 -0
  140. package/src/chain/prepareNextSlot.ts +2 -1
  141. package/src/chain/produceBlock/produceBlockBody.ts +158 -25
  142. package/src/chain/regen/interface.ts +2 -0
  143. package/src/chain/validation/executionPayloadBid.ts +130 -33
  144. package/src/chain/validation/payloadAttestationMessage.ts +32 -8
  145. package/src/chain/validatorMonitor.ts +18 -0
  146. package/src/execution/builder/index.ts +1 -4
  147. package/src/execution/engine/interface.ts +1 -0
  148. package/src/execution/engine/types.ts +4 -0
  149. package/src/metrics/metrics/lodestar.ts +4 -3
  150. package/src/network/interface.ts +2 -0
  151. package/src/network/network.ts +22 -0
  152. package/src/network/processor/gossipHandlers.ts +25 -6
  153. package/src/network/reqresp/handlers/dataColumnSidecarsByRange.ts +6 -0
  154. package/src/network/reqresp/handlers/dataColumnSidecarsByRoot.ts +1 -1
  155. package/src/network/reqresp/handlers/executionPayloadEnvelopesByRange.ts +3 -3
  156. package/src/network/reqresp/handlers/executionPayloadEnvelopesByRoot.ts +20 -1
  157. package/src/network/reqresp/handlers/index.ts +2 -2
  158. package/src/network/reqresp/utils/dataColumnResponseValidation.ts +8 -0
  159. package/src/util/dependentRoot.ts +22 -18
  160. package/lib/execution/builder/utils.d.ts +0 -5
  161. package/lib/execution/builder/utils.d.ts.map +0 -1
  162. package/lib/execution/builder/utils.js +0 -17
  163. package/lib/execution/builder/utils.js.map +0 -1
  164. package/src/execution/builder/utils.ts +0 -19
@@ -0,0 +1,59 @@
1
+ import {RootHex, Slot, gloas} from "@lodestar/types";
2
+ import {toRootHex} from "@lodestar/utils";
3
+
4
+ /**
5
+ * Pool of validated `SignedProposerPreferences` indexed by `(slot, dependent_root)`.
6
+ *
7
+ * The primary consumer is `validateExecutionPayloadBid`, which looks up the matching
8
+ * preferences via `get(bid.slot, dependent_root)` to enforce the IGNORE-existence and
9
+ * REJECT-equality rules from the gloas spec. The beacon API `/pool/proposer_preferences`
10
+ * GET endpoint reads from the same pool via `getAll`.
11
+ *
12
+ * `validator_index` is intentionally not part of the key: gossip validation enforces
13
+ * `proposers[proposalSlot % SLOTS_PER_EPOCH] === validatorIndex` against the shuffling
14
+ * implied by `dependent_root`, so once a preference has been validated `(slot, dependent_root)`
15
+ * already pins down the validator.
16
+ */
17
+ export class ProposerPreferencesPool {
18
+ private readonly bySlot = new Map<Slot, Map<RootHex, gloas.SignedProposerPreferences>>();
19
+
20
+ /** Lookup for bid validation: matches `(bid.slot, get_proposer_dependent_root(parent_state, ...))`. */
21
+ get(slot: Slot, dependentRootHex: RootHex): gloas.SignedProposerPreferences | null {
22
+ return this.bySlot.get(slot)?.get(dependentRootHex) ?? null;
23
+ }
24
+
25
+ add(signed: gloas.SignedProposerPreferences): void {
26
+ const {proposalSlot, dependentRoot} = signed.message;
27
+ const rootHex = toRootHex(dependentRoot);
28
+ let byRoot = this.bySlot.get(proposalSlot);
29
+ if (!byRoot) {
30
+ byRoot = new Map();
31
+ this.bySlot.set(proposalSlot, byRoot);
32
+ }
33
+ byRoot.set(rootHex, signed);
34
+ }
35
+
36
+ /** API read-out: flatten across branches, optionally filtered by slot. */
37
+ getAll(slot?: Slot): gloas.SignedProposerPreferences[] {
38
+ if (slot !== undefined) {
39
+ const byRoot = this.bySlot.get(slot);
40
+ return byRoot ? Array.from(byRoot.values()) : [];
41
+ }
42
+ const out: gloas.SignedProposerPreferences[] = [];
43
+ for (const byRoot of this.bySlot.values()) {
44
+ for (const v of byRoot.values()) out.push(v);
45
+ }
46
+ return out;
47
+ }
48
+
49
+ /**
50
+ * Entries are only load-bearing while `proposal_slot >= current_slot`. Once the slot has
51
+ * passed the `[IGNORE] proposal_slot > current_slot` gossip rule takes over, so drop them
52
+ * on each slot tick.
53
+ */
54
+ prune(currentSlot: Slot): void {
55
+ for (const slot of this.bySlot.keys()) {
56
+ if (slot < currentSlot) this.bySlot.delete(slot);
57
+ }
58
+ }
59
+ }
@@ -169,7 +169,8 @@ export class PrepareNextSlotScheduler {
169
169
  // Apply parent payload once here as it's reused by EL prep and SSE emit below
170
170
  let stateAfterParentPayload: IBeaconStateViewBellatrix = updatedPrepareState;
171
171
  if (isStatePostGloas(updatedPrepareState)) {
172
- if (this.chain.forkChoice.shouldExtendPayload(updatedHead.blockRoot)) {
172
+ // Spec: should_build_on_full(store, head) — see produceBlockBody.ts for context.
173
+ if (this.chain.forkChoice.shouldBuildOnFull(updatedHead, prepareSlot)) {
173
174
  parentBlockHash = updatedPrepareState.latestExecutionPayloadBid.blockHash;
174
175
  // Skip applying parent payload unless we're proposing the next slot or have to emit payload_attributes events
175
176
  if (feeRecipient !== undefined || this.chain.opts.emitPayloadAttributes === true) {
@@ -18,7 +18,9 @@ import {
18
18
  G2_POINT_AT_INFINITY,
19
19
  IBeaconStateView,
20
20
  type IBeaconStateViewBellatrix,
21
+ computeEpochAtSlot,
21
22
  computeTimeAtSlot,
23
+ getExpectedGasLimit,
22
24
  isStatePostBellatrix,
23
25
  isStatePostCapella,
24
26
  isStatePostGloas,
@@ -48,20 +50,16 @@ import {
48
50
  gloas,
49
51
  ssz,
50
52
  } from "@lodestar/types";
51
- import {Logger, byteArrayEquals, fromHex, sleep, toHex, toPubkeyHex, toRootHex} from "@lodestar/utils";
53
+ import {GWEI_TO_WEI, Logger, byteArrayEquals, fromHex, sleep, toHex, toPubkeyHex, toRootHex} from "@lodestar/utils";
52
54
  import {ZERO_HASH_HEX} from "../../constants/index.js";
53
55
  import {numToQuantity} from "../../execution/engine/utils.js";
54
- import {
55
- IExecutionBuilder,
56
- IExecutionEngine,
57
- PayloadAttributes,
58
- PayloadId,
59
- getExpectedGasLimit,
60
- } from "../../execution/index.js";
56
+ import {IExecutionBuilder, IExecutionEngine, PayloadAttributes, PayloadId} from "../../execution/index.js";
57
+ import {getShufflingDependentRoot} from "../../util/dependentRoot.js";
61
58
  import {fromGraffitiBytes} from "../../util/graffiti.js";
62
59
  import {kzg} from "../../util/kzg.js";
63
60
  import type {BeaconChain} from "../chain.js";
64
61
  import {CommonBlockBody} from "../interface.js";
62
+ import {ProposerPreferencesPool} from "../opPools/index.js";
65
63
  import {validateBlobsAndKzgCommitments, validateCellsAndKzgCommitments} from "./validateBlobsAndKzgCommitments.js";
66
64
 
67
65
  // Time to provide the EL to generate a payload from new payload id
@@ -93,6 +91,8 @@ export type BlockAttributes = {
93
91
  slot: Slot;
94
92
  parentBlock: ProtoBlock;
95
93
  feeRecipient?: string;
94
+ /** When provided, build block with this builder bid instead of a self-build bid */
95
+ builderBid?: gloas.SignedExecutionPayloadBid;
96
96
  };
97
97
 
98
98
  export enum BlockType {
@@ -152,6 +152,28 @@ export type ProduceResult =
152
152
  | ProduceFullPhase0
153
153
  | ProduceBlinded;
154
154
 
155
+ /**
156
+ * Drop voluntary exits that `parent_execution_requests` have invalidated (e.g. a withdrawal
157
+ * request initiating an exit on the same validator). Op pool selected against the unapplied
158
+ * state, so re-validate against the post-apply state to avoid producing an invalid block.
159
+ *
160
+ * `getStateAfterParentPayload` is a thunk so the post-apply state is only materialized when
161
+ * actually needed (i.e. when extending the parent payload and there are exits to filter).
162
+ */
163
+ function maybeFilterInvalidatedVoluntaryExits(
164
+ commonBlockBody: CommonBlockBody,
165
+ isExtendingPayload: boolean,
166
+ getStateAfterParentPayload: () => IBeaconStateViewBellatrix
167
+ ): CommonBlockBody["voluntaryExits"] {
168
+ if (!isExtendingPayload || commonBlockBody.voluntaryExits.length === 0) {
169
+ return commonBlockBody.voluntaryExits;
170
+ }
171
+ const state = getStateAfterParentPayload();
172
+ return commonBlockBody.voluntaryExits.filter((signedVoluntaryExit) =>
173
+ state.isValidVoluntaryExit(signedVoluntaryExit, false)
174
+ );
175
+ }
176
+
155
177
  export async function produceBlockBody<T extends BlockType>(
156
178
  this: BeaconChain,
157
179
  blockType: T,
@@ -174,6 +196,7 @@ export async function produceBlockBody<T extends BlockType>(
174
196
  proposerIndex,
175
197
  proposerPubKey,
176
198
  commonBlockBodyPromise,
199
+ builderBid,
177
200
  } = blockAttr;
178
201
  let executionPayloadValue: Wei;
179
202
  let blockBody: AssembledBodyType<T>;
@@ -194,7 +217,43 @@ export async function produceBlockBody<T extends BlockType>(
194
217
  };
195
218
  this.logger.verbose("Producing beacon block body", logMeta);
196
219
 
197
- if (isForkPostGloas(fork)) {
220
+ if (builderBid !== undefined) {
221
+ if (!isStatePostGloas(currentState)) {
222
+ throw new Error("Expected Gloas state for builder bid block production");
223
+ }
224
+
225
+ const isExtendingPayload = byteArrayEquals(
226
+ builderBid.message.parentBlockHash,
227
+ currentState.latestExecutionPayloadBid.blockHash
228
+ );
229
+ const parentExecutionRequests = isExtendingPayload
230
+ ? await this.getParentExecutionRequests(parentBlock.slot, parentBlock.blockRoot)
231
+ : ssz.electra.ExecutionRequests.defaultValue();
232
+ executionPayloadValue = BigInt(builderBid.message.value) * GWEI_TO_WEI;
233
+
234
+ const commonBlockBody = await commonBlockBodyPromise;
235
+ const gloasBody = Object.assign({}, commonBlockBody) as gloas.BeaconBlockBody;
236
+ gloasBody.signedExecutionPayloadBid = builderBid;
237
+ gloasBody.payloadAttestations = this.payloadAttestationPool.getPayloadAttestationsForBlock(
238
+ parentBlock.blockRoot,
239
+ blockSlot - 1
240
+ );
241
+ gloasBody.parentExecutionRequests = parentExecutionRequests;
242
+ gloasBody.voluntaryExits = maybeFilterInvalidatedVoluntaryExits(commonBlockBody, isExtendingPayload, () =>
243
+ currentState.withParentPayloadApplied(parentExecutionRequests)
244
+ );
245
+ blockBody = gloasBody as AssembledBodyType<T>;
246
+
247
+ this.logger.verbose("Produced block with builder bid", {
248
+ slot: blockSlot,
249
+ builderIndex: builderBid.message.builderIndex,
250
+ bidValue: builderBid.message.value,
251
+ parentBlockHash: toRootHex(builderBid.message.parentBlockHash),
252
+ parentBlockRoot: toRootHex(builderBid.message.parentBlockRoot),
253
+ blockHash: toRootHex(builderBid.message.blockHash),
254
+ isExtendingPayload,
255
+ });
256
+ } else if (isForkPostGloas(fork)) {
198
257
  if (!isStatePostGloas(currentState)) {
199
258
  throw new Error("Expected Gloas state for Gloas block production");
200
259
  }
@@ -204,23 +263,23 @@ export async function produceBlockBody<T extends BlockType>(
204
263
  // this into a completely separate function and have pre/post gloas more separated
205
264
  const safeBlockHash = getSafeExecutionBlockHash(this.forkChoice);
206
265
  const finalizedBlockHash = this.forkChoice.getFinalizedBlock().executionPayloadBlockHash ?? ZERO_HASH_HEX;
266
+ // TODO GLOAS: post-Gloas, proposer feeRecipient is also carried (signed) in
267
+ // ProposerPreferencesPool. Consider using this unified cache instead
268
+ // see https://github.com/ChainSafe/lodestar/issues/9379
207
269
  const feeRecipient = requestedFeeRecipient ?? this.beaconProposerCache.getOrDefault(proposerIndex);
208
270
 
209
271
  const endExecutionPayload = this.metrics?.executionBlockProductionTimeSteps.startTimer();
210
272
 
211
- this.logger.verbose("Preparing execution payload from engine", {
212
- slot: blockSlot,
213
- parentBlockRoot: toRootHex(parentBlockRoot),
214
- feeRecipient,
215
- });
216
-
217
273
  // Get execution payload from EL
218
274
  let parentBlockHash: Bytes32;
219
275
  let parentExecutionRequests: electra.ExecutionRequests;
220
276
  // Apply parent payload once here as it's reused by EL prep and voluntary exit filtering below
221
277
  let stateAfterParentPayload: IBeaconStateViewBellatrix = currentState;
222
- const isExtendingPayload = this.forkChoice.shouldExtendPayload(toRootHex(parentBlockRoot));
223
- if (isExtendingPayload) {
278
+ // Spec: should_build_on_full(store, head). `parentBlock` is the proposer's head
279
+ // (set by chain.getProposerHead(slot)). Returns false when the PTC majority signalled
280
+ // the blob data is not available or the payload was not timely, forcing a build on EMPTY (reorg).
281
+ const isBuildingOnFull = this.forkChoice.shouldBuildOnFull(parentBlock, blockSlot);
282
+ if (isBuildingOnFull) {
224
283
  parentBlockHash = currentState.latestExecutionPayloadBid.blockHash;
225
284
  parentExecutionRequests = await this.getParentExecutionRequests(parentBlock.slot, parentBlock.blockRoot);
226
285
  stateAfterParentPayload = currentState.withParentPayloadApplied(parentExecutionRequests);
@@ -243,6 +302,16 @@ export async function produceBlockBody<T extends BlockType>(
243
302
  const {prepType, payloadId} = prepareRes;
244
303
  Object.assign(logMeta, {executionPayloadPrepType: prepType});
245
304
 
305
+ this.logger.verbose("Prepared execution payload from engine", {
306
+ slot: blockSlot,
307
+ parentBlockRoot: toRootHex(parentBlockRoot),
308
+ parentBlockHash: toRootHex(parentBlockHash),
309
+ feeRecipient,
310
+ prepType,
311
+ payloadId,
312
+ isBuildingOnFull,
313
+ });
314
+
246
315
  if (prepType !== PayloadPreparationType.Cached) {
247
316
  await sleep(PAYLOAD_GENERATION_TIME_MS);
248
317
  }
@@ -296,14 +365,11 @@ export async function produceBlockBody<T extends BlockType>(
296
365
  blockSlot - 1
297
366
  );
298
367
  gloasBody.parentExecutionRequests = parentExecutionRequests;
299
- // Drop voluntary exits that parent_execution_requests have invalidated (e.g. a withdrawal
300
- // request initiating an exit on the same validator). Op pool selected against the unapplied
301
- // state, so re-validate against the post-apply state to avoid producing an invalid block.
302
- if (isExtendingPayload && commonBlockBody.voluntaryExits.length > 0) {
303
- gloasBody.voluntaryExits = commonBlockBody.voluntaryExits.filter((signedVoluntaryExit) =>
304
- stateAfterParentPayload.isValidVoluntaryExit(signedVoluntaryExit, false)
305
- );
306
- }
368
+ gloasBody.voluntaryExits = maybeFilterInvalidatedVoluntaryExits(
369
+ commonBlockBody,
370
+ isBuildingOnFull,
371
+ () => stateAfterParentPayload
372
+ );
307
373
  blockBody = gloasBody as AssembledBodyType<T>;
308
374
 
309
375
  // Store execution payload data required to construct execution payload envelope later
@@ -324,6 +390,7 @@ export async function produceBlockBody<T extends BlockType>(
324
390
  fetchedTime,
325
391
  executionBlockHash: toRootHex(executionPayload.blockHash),
326
392
  blobs: blobsBundle.commitments.length,
393
+ gasLimit: executionPayload.gasLimit,
327
394
  });
328
395
 
329
396
  Object.assign(logMeta, {
@@ -633,6 +700,8 @@ export async function prepareExecutionPayload(
633
700
  chain: {
634
701
  executionEngine: IExecutionEngine;
635
702
  config: ChainForkConfig;
703
+ forkChoice: IForkChoice;
704
+ proposerPreferencesPool: ProposerPreferencesPool;
636
705
  },
637
706
  logger: Logger,
638
707
  fork: ForkPostBellatrix,
@@ -733,6 +802,7 @@ export function getPayloadAttributesForSSE(
733
802
  chain: {
734
803
  config: ChainForkConfig;
735
804
  forkChoice: IForkChoice;
805
+ proposerPreferencesPool: ProposerPreferencesPool;
736
806
  },
737
807
  {
738
808
  prepareState,
@@ -789,6 +859,8 @@ function preparePayloadAttributes(
789
859
  fork: ForkPostBellatrix,
790
860
  chain: {
791
861
  config: ChainForkConfig;
862
+ forkChoice: IForkChoice;
863
+ proposerPreferencesPool: ProposerPreferencesPool;
792
864
  },
793
865
  {
794
866
  prepareState,
@@ -851,12 +923,73 @@ function preparePayloadAttributes(
851
923
  }
852
924
 
853
925
  if (ForkSeq[fork] >= ForkSeq.gloas) {
926
+ if (!isStatePostGloas(prepareState)) {
927
+ throw new Error("Expected Gloas state for Gloas payload attributes");
928
+ }
854
929
  (payloadAttributes as gloas.SSEPayloadAttributes["payloadAttributes"]).slotNumber = prepareSlot;
930
+ (payloadAttributes as gloas.SSEPayloadAttributes["payloadAttributes"]).targetGasLimit = getProposerTargetGasLimit(
931
+ chain,
932
+ prepareSlot,
933
+ parentBlockRoot,
934
+ parentBlockHash
935
+ );
855
936
  }
856
937
 
857
938
  return payloadAttributes;
858
939
  }
859
940
 
941
+ /**
942
+ * Resolve the proposer's preferred (target) gas limit for the Gloas `PayloadAttributesV4`
943
+ * `targetGasLimit` field (consensus-specs#5235, execution-apis#796).
944
+ *
945
+ * Sourced from the `SignedProposerPreferences` the proposer's VC submitted to the pool
946
+ * (same `(slot, dependent_root)` lookup as gossip bid validation). When no matching
947
+ * preferences are pooled, target the parent payload's gas limit so the gas limit stays
948
+ * unchanged (`is_gas_limit_target_compatible` then requires `gas_limit == parent_gas_limit`).
949
+ *
950
+ * The parent payload's gas_limit is read from fork choice — the variant matching
951
+ * `(parentBlockRoot, parentBlockHash)` carries the correct value for both FULL parents
952
+ * (FULL.executionPayloadGasLimit = delivered payload's gas_limit) and EMPTY parents
953
+ * (EMPTY.executionPayloadGasLimit = inherited grandparent's gas_limit).
954
+ */
955
+ function getProposerTargetGasLimit(
956
+ chain: {forkChoice: IForkChoice; proposerPreferencesPool: ProposerPreferencesPool},
957
+ prepareSlot: Slot,
958
+ parentBlockRoot: Root,
959
+ parentBlockHash: Bytes32
960
+ ): number {
961
+ const parentBlockRootHex = toRootHex(parentBlockRoot);
962
+ const parentBlock = chain.forkChoice.getBlockHexDefaultStatus(parentBlockRootHex);
963
+ const dependentRootHex = (() => {
964
+ if (parentBlock === null) {
965
+ return null;
966
+ }
967
+ try {
968
+ return getShufflingDependentRoot(
969
+ chain.forkChoice,
970
+ computeEpochAtSlot(prepareSlot),
971
+ computeEpochAtSlot(parentBlock.slot),
972
+ parentBlock
973
+ );
974
+ } catch {
975
+ return null;
976
+ }
977
+ })();
978
+
979
+ const pref = dependentRootHex !== null ? chain.proposerPreferencesPool.get(prepareSlot, dependentRootHex) : null;
980
+ if (pref !== null) {
981
+ return pref.message.targetGasLimit;
982
+ }
983
+
984
+ const parentPayloadVariant = chain.forkChoice.getBlockHexAndBlockHash(parentBlockRootHex, toRootHex(parentBlockHash));
985
+ if (parentPayloadVariant === null || parentPayloadVariant.executionPayloadBlockHash === null) {
986
+ throw new Error(
987
+ `Cannot resolve parent payload gas_limit for proposer targetGasLimit fallback parentBlockRoot=${parentBlockRootHex} parentBlockHash=${toRootHex(parentBlockHash)}`
988
+ );
989
+ }
990
+ return parentPayloadVariant.executionPayloadGasLimit;
991
+ }
992
+
860
993
  export async function produceCommonBlockBody<T extends BlockType>(
861
994
  this: BeaconChain,
862
995
  blockType: T,
@@ -17,10 +17,12 @@ export enum RegenCaller {
17
17
  predictProposerHead = "predictProposerHead",
18
18
  produceAttestationData = "produceAttestationData",
19
19
  processBlocksInEpoch = "processBlocksInEpoch",
20
+ importExecutionPayload = "importExecutionPayload",
20
21
  validateGossipAggregateAndProof = "validateGossipAggregateAndProof",
21
22
  validateGossipAttestation = "validateGossipAttestation",
22
23
  validateGossipVoluntaryExit = "validateGossipVoluntaryExit",
23
24
  validateGossipExecutionPayloadBid = "validateGossipExecutionPayloadBid",
25
+ validateGossipPayloadAttestationMessage = "validateGossipPayloadAttestationMessage",
24
26
  validateGossipProposerPreferences = "validateGossipProposerPreferences",
25
27
  onForkChoiceFinalized = "onForkChoiceFinalized",
26
28
  restApi = "restApi",
@@ -4,10 +4,12 @@ import {
4
4
  createSingleSignatureSetFromComponents,
5
5
  getExecutionPayloadBidSigningRoot,
6
6
  isActiveBuilder,
7
+ isGasLimitTargetCompatible,
7
8
  isStatePostGloas,
8
9
  } from "@lodestar/state-transition";
9
- import {gloas} from "@lodestar/types";
10
- import {toRootHex} from "@lodestar/utils";
10
+ import {ValidatorIndex, gloas} from "@lodestar/types";
11
+ import {byteArrayEquals, toHex, toRootHex} from "@lodestar/utils";
12
+ import {getShufflingDependentRoot} from "../../util/dependentRoot.js";
11
13
  import {ExecutionPayloadBidError, ExecutionPayloadBidErrorCode, GossipAction} from "../errors/index.js";
12
14
  import {IBeaconChain} from "../index.js";
13
15
  import {RegenCaller} from "../regen/index.js";
@@ -15,28 +17,24 @@ import {RegenCaller} from "../regen/index.js";
15
17
  export async function validateApiExecutionPayloadBid(
16
18
  chain: IBeaconChain,
17
19
  signedExecutionPayloadBid: gloas.SignedExecutionPayloadBid
18
- ): Promise<void> {
20
+ ): Promise<{proposerIndex: ValidatorIndex}> {
19
21
  return validateExecutionPayloadBid(chain, signedExecutionPayloadBid);
20
22
  }
21
23
 
22
24
  export async function validateGossipExecutionPayloadBid(
23
25
  chain: IBeaconChain,
24
26
  signedExecutionPayloadBid: gloas.SignedExecutionPayloadBid
25
- ): Promise<void> {
27
+ ): Promise<{proposerIndex: ValidatorIndex}> {
26
28
  return validateExecutionPayloadBid(chain, signedExecutionPayloadBid);
27
29
  }
28
30
 
29
31
  async function validateExecutionPayloadBid(
30
32
  chain: IBeaconChain,
31
33
  signedExecutionPayloadBid: gloas.SignedExecutionPayloadBid
32
- ): Promise<void> {
34
+ ): Promise<{proposerIndex: ValidatorIndex}> {
33
35
  const bid = signedExecutionPayloadBid.message;
34
36
  const parentBlockRootHex = toRootHex(bid.parentBlockRoot);
35
37
  const parentBlockHashHex = toRootHex(bid.parentBlockHash);
36
- const state = await chain.getHeadStateAtCurrentEpoch(RegenCaller.validateGossipExecutionPayloadBid);
37
- if (!isStatePostGloas(state)) {
38
- throw new Error(`Expected gloas+ state for execution payload bid validation, got fork=${state.forkName}`);
39
- }
40
38
 
41
39
  // [IGNORE] `bid.slot` is the current slot or the next slot.
42
40
  const currentSlot = chain.clock.currentSlot;
@@ -48,16 +46,92 @@ async function validateExecutionPayloadBid(
48
46
  });
49
47
  }
50
48
 
49
+ // [IGNORE] `bid.parent_block_root` is the hash tree root of a known beacon block in fork choice.
50
+ // Moved earlier than the spec ordering so we can derive the proposer dependent root for the
51
+ // proposer-preferences lookup below from a known fork-choice block.
52
+ const parentBlock = chain.forkChoice.getBlockHexDefaultStatus(parentBlockRootHex);
53
+ if (parentBlock === null) {
54
+ throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
55
+ code: ExecutionPayloadBidErrorCode.UNKNOWN_BLOCK_ROOT,
56
+ parentBlockRoot: parentBlockRootHex,
57
+ });
58
+ }
59
+
60
+ // [REJECT] The bid is for a higher slot than its parent block -- i.e.
61
+ // validate that `bid.slot` is greater than the slot of the block with root
62
+ // `bid.parent_block_root`.
63
+ if (bid.slot <= parentBlock.slot) {
64
+ throw new ExecutionPayloadBidError(GossipAction.REJECT, {
65
+ code: ExecutionPayloadBidErrorCode.NOT_LATER_THAN_PARENT,
66
+ parentSlot: parentBlock.slot,
67
+ slot: bid.slot,
68
+ });
69
+ }
70
+
51
71
  // [IGNORE] A `SignedProposerPreferences` matching `bid.slot` and the bid's branch has been
52
72
  // seen — i.e. `proposal_slot == bid.slot` AND `dependent_root ==
53
- // get_proposer_dependent_root(parent_state, compute_epoch_at_slot(bid.slot))`,
54
- // where `parent_state` is the post-state of `bid.parent_block_root`.
55
- // This is the message referenced as `proposer_preferences` in the following REJECT rules.
56
- // TODO GLOAS: Implement once a ProposerPreferencesPool exists.
73
+ // get_proposer_dependent_root(parent_state, compute_epoch_at_slot(bid.slot))`.
74
+ const bidEpoch = computeEpochAtSlot(bid.slot);
75
+ // gloas is always post-Fulu, so `get_proposer_dependent_root` is the post-Fulu (deterministic
76
+ // proposer lookahead) form `block_root_at(start_slot(epoch - MIN_SEED_LOOKAHEAD) - 1)` with
77
+ // `MIN_SEED_LOOKAHEAD == 1` — identical to the attester-shuffling dependent root for the same
78
+ // epoch (both 1-epoch lookahead), hence `getShufflingDependentRoot`. `null` on a
79
+ // unknown/finalized-pruned ancestor or genesis edge → degrade to IGNORE below instead of
80
+ // letting a raw `ForkChoiceError` escape the `GossipActionError` contract.
81
+ const dependentRootHex = (() => {
82
+ try {
83
+ return getShufflingDependentRoot(chain.forkChoice, bidEpoch, computeEpochAtSlot(parentBlock.slot), parentBlock);
84
+ } catch {
85
+ return null;
86
+ }
87
+ })();
88
+
89
+ if (dependentRootHex === null) {
90
+ // Could not derive the dependent root for this branch (unknown/finalized-pruned ancestor,
91
+ // genesis edge, etc.) → definitionally no matching `SignedProposerPreferences`.
92
+ throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
93
+ code: ExecutionPayloadBidErrorCode.NO_MATCHING_PROPOSER_PREFERENCES,
94
+ slot: bid.slot,
95
+ parentBlockRoot: parentBlockRootHex,
96
+ dependentRoot: "unknown",
97
+ });
98
+ }
99
+
100
+ const proposerPreferences = chain.proposerPreferencesPool.get(bid.slot, dependentRootHex);
101
+ if (proposerPreferences === null) {
102
+ throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
103
+ code: ExecutionPayloadBidErrorCode.NO_MATCHING_PROPOSER_PREFERENCES,
104
+ slot: bid.slot,
105
+ parentBlockRoot: parentBlockRootHex,
106
+ dependentRoot: dependentRootHex,
107
+ });
108
+ }
109
+
110
+ // Use the bid's parent branch state for builder checks
111
+ const state = await chain.regen
112
+ .getBlockSlotState(parentBlock, bid.slot, {dontTransferCache: true}, RegenCaller.validateGossipExecutionPayloadBid)
113
+ .catch(() => {
114
+ throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
115
+ code: ExecutionPayloadBidErrorCode.UNKNOWN_BLOCK_ROOT,
116
+ parentBlockRoot: parentBlockRootHex,
117
+ });
118
+ });
119
+
120
+ if (!isStatePostGloas(state)) {
121
+ throw new Error(`Expected gloas+ state for execution payload bid validation, got fork=${state.forkName}`);
122
+ }
57
123
 
58
124
  // [REJECT] `bid.builder_index` is a valid/active builder index -- i.e.
59
125
  // `is_active_builder(state, bid.builder_index)` returns `True`.
60
- const builder = state.getBuilder(bid.builderIndex);
126
+ let builder: gloas.Builder;
127
+ try {
128
+ builder = state.getBuilder(bid.builderIndex);
129
+ } catch {
130
+ throw new ExecutionPayloadBidError(GossipAction.REJECT, {
131
+ code: ExecutionPayloadBidErrorCode.BUILDER_NOT_ELIGIBLE,
132
+ builderIndex: bid.builderIndex,
133
+ });
134
+ }
61
135
  if (!isActiveBuilder(builder, state.finalizedCheckpoint.epoch)) {
62
136
  throw new ExecutionPayloadBidError(GossipAction.REJECT, {
63
137
  code: ExecutionPayloadBidErrorCode.BUILDER_NOT_ELIGIBLE,
@@ -75,10 +149,44 @@ async function validateExecutionPayloadBid(
75
149
  }
76
150
 
77
151
  // [REJECT] `bid.fee_recipient == proposer_preferences.fee_recipient`.
78
- // [REJECT] `bid.gas_limit == proposer_preferences.gas_limit`.
79
- // Both compared against the matching `proposer_preferences` defined above (same branch
80
- // via dependent_root, same proposal_slot).
81
- // TODO GLOAS: Implement once a ProposerPreferencesPool exists.
152
+ if (!byteArrayEquals(bid.feeRecipient, proposerPreferences.message.feeRecipient)) {
153
+ throw new ExecutionPayloadBidError(GossipAction.REJECT, {
154
+ code: ExecutionPayloadBidErrorCode.PROPOSER_PREFERENCES_FEE_RECIPIENT_MISMATCH,
155
+ builderIndex: bid.builderIndex,
156
+ bidFeeRecipient: toHex(bid.feeRecipient),
157
+ expectedFeeRecipient: toHex(proposerPreferences.message.feeRecipient),
158
+ });
159
+ }
160
+
161
+ // [IGNORE] `bid.parent_block_hash` is the block hash of a known execution payload in fork
162
+ // choice. Looks up the variant of `bid.parent_block_root` whose payload hash matches
163
+ // `bid.parent_block_hash` — works for both FULL parents (FULL variant carries the delivered
164
+ // payload's hash) and EMPTY parents (EMPTY/PENDING variants carry the inherited parent
165
+ // payload's hash, since the new block doesn't have its own payload). Variant carries the
166
+ // executed payload's gas_limit, which we use as `parent_gas_limit` below.
167
+ const parentPayloadVariant = chain.forkChoice.getBlockHexAndBlockHash(parentBlockRootHex, parentBlockHashHex);
168
+ if (parentPayloadVariant === null || parentPayloadVariant.executionPayloadBlockHash === null) {
169
+ throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
170
+ code: ExecutionPayloadBidErrorCode.UNKNOWN_PARENT_BLOCK_HASH,
171
+ parentBlockHash: parentBlockHashHex,
172
+ });
173
+ }
174
+
175
+ // [IGNORE] `is_gas_limit_target_compatible(parent_gas_limit, bid.gas_limit, target_gas_limit)`,
176
+ // where `parent_gas_limit` is the `gas_limit` of the parent execution payload and
177
+ // `target_gas_limit` is `proposer_preferences.target_gas_limit`.
178
+ const bidGasLimit = Number(bid.gasLimit);
179
+ const parentGasLimit = parentPayloadVariant.executionPayloadGasLimit;
180
+ const targetGasLimit = proposerPreferences.message.targetGasLimit;
181
+ if (!isGasLimitTargetCompatible(parentGasLimit, bidGasLimit, targetGasLimit)) {
182
+ throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
183
+ code: ExecutionPayloadBidErrorCode.PROPOSER_PREFERENCES_GAS_LIMIT_MISMATCH,
184
+ builderIndex: bid.builderIndex,
185
+ bidGasLimit,
186
+ parentGasLimit,
187
+ targetGasLimit,
188
+ });
189
+ }
82
190
 
83
191
  // [REJECT] The length of KZG commitments is less than or equal to the limitation defined in the
84
192
  // consensus layer -- i.e. validate that
@@ -107,11 +215,11 @@ async function validateExecutionPayloadBid(
107
215
  // [IGNORE] this bid is the highest value bid seen for the tuple
108
216
  // `(bid.slot, bid.parent_block_hash, bid.parent_block_root)`.
109
217
  const bestBid = chain.executionPayloadBidPool.getBestBid(bid.slot, parentBlockHashHex, parentBlockRootHex);
110
- if (bestBid !== null && bestBid.value >= bid.value) {
218
+ if (bestBid !== null && bestBid.message.value >= bid.value) {
111
219
  throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
112
220
  code: ExecutionPayloadBidErrorCode.BID_TOO_LOW,
113
221
  bidValue: bid.value,
114
- currentHighestBid: bestBid.value,
222
+ currentHighestBid: bestBid.message.value,
115
223
  });
116
224
  }
117
225
  // [IGNORE] `bid.value` is less or equal than the builder's excess balance --
@@ -124,19 +232,6 @@ async function validateExecutionPayloadBid(
124
232
  });
125
233
  }
126
234
 
127
- // [IGNORE] `bid.parent_block_hash` is the block hash of a known execution
128
- // payload in fork choice.
129
- // TODO GLOAS: implement this
130
-
131
- // [IGNORE] `bid.parent_block_root` is the hash tree root of a known beacon
132
- // block in fork choice.
133
- if (!chain.forkChoice.hasBlock(bid.parentBlockRoot)) {
134
- throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
135
- code: ExecutionPayloadBidErrorCode.UNKNOWN_BLOCK_ROOT,
136
- parentBlockRoot: parentBlockRootHex,
137
- });
138
- }
139
-
140
235
  // [REJECT] `signed_execution_payload_bid.signature` is valid with respect to the `bid.builder_index`.
141
236
  const signatureSet = createSingleSignatureSetFromComponents(
142
237
  PublicKey.fromBytes(builder.pubkey),
@@ -154,4 +249,6 @@ async function validateExecutionPayloadBid(
154
249
 
155
250
  // Valid
156
251
  chain.seenExecutionPayloadBids.add(bid.slot, bid.builderIndex);
252
+
253
+ return {proposerIndex: proposerPreferences.message.validatorIndex};
157
254
  }