@lodestar/beacon-node 1.44.0-dev.1d0e0b9081 → 1.44.0-dev.6c85077978

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 (86) 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 +1 -1
  6. package/lib/api/impl/beacon/pool/index.js.map +1 -1
  7. package/lib/api/impl/validator/index.d.ts.map +1 -1
  8. package/lib/api/impl/validator/index.js +77 -37
  9. package/lib/api/impl/validator/index.js.map +1 -1
  10. package/lib/chain/blocks/importBlock.d.ts.map +1 -1
  11. package/lib/chain/blocks/importBlock.js +1 -1
  12. package/lib/chain/blocks/importBlock.js.map +1 -1
  13. package/lib/chain/blocks/importExecutionPayload.d.ts.map +1 -1
  14. package/lib/chain/blocks/importExecutionPayload.js +4 -2
  15. package/lib/chain/blocks/importExecutionPayload.js.map +1 -1
  16. package/lib/chain/chain.d.ts +1 -1
  17. package/lib/chain/chain.d.ts.map +1 -1
  18. package/lib/chain/chain.js +2 -1
  19. package/lib/chain/chain.js.map +1 -1
  20. package/lib/chain/errors/executionPayloadBid.d.ts +6 -1
  21. package/lib/chain/errors/executionPayloadBid.d.ts.map +1 -1
  22. package/lib/chain/errors/executionPayloadBid.js +1 -0
  23. package/lib/chain/errors/executionPayloadBid.js.map +1 -1
  24. package/lib/chain/forkChoice/index.d.ts.map +1 -1
  25. package/lib/chain/forkChoice/index.js +14 -4
  26. package/lib/chain/forkChoice/index.js.map +1 -1
  27. package/lib/chain/opPools/executionPayloadBidPool.d.ts +4 -4
  28. package/lib/chain/opPools/executionPayloadBidPool.d.ts.map +1 -1
  29. package/lib/chain/opPools/executionPayloadBidPool.js +6 -4
  30. package/lib/chain/opPools/executionPayloadBidPool.js.map +1 -1
  31. package/lib/chain/prepareNextSlot.d.ts.map +1 -1
  32. package/lib/chain/prepareNextSlot.js +2 -1
  33. package/lib/chain/prepareNextSlot.js.map +1 -1
  34. package/lib/chain/produceBlock/produceBlockBody.d.ts +3 -1
  35. package/lib/chain/produceBlock/produceBlockBody.d.ts.map +1 -1
  36. package/lib/chain/produceBlock/produceBlockBody.js +78 -24
  37. package/lib/chain/produceBlock/produceBlockBody.js.map +1 -1
  38. package/lib/chain/validation/executionPayloadBid.d.ts +7 -3
  39. package/lib/chain/validation/executionPayloadBid.d.ts.map +1 -1
  40. package/lib/chain/validation/executionPayloadBid.js +26 -10
  41. package/lib/chain/validation/executionPayloadBid.js.map +1 -1
  42. package/lib/chain/validatorMonitor.d.ts +1 -0
  43. package/lib/chain/validatorMonitor.d.ts.map +1 -1
  44. package/lib/chain/validatorMonitor.js +16 -0
  45. package/lib/chain/validatorMonitor.js.map +1 -1
  46. package/lib/execution/builder/index.d.ts +1 -2
  47. package/lib/execution/builder/index.d.ts.map +1 -1
  48. package/lib/execution/builder/index.js +0 -1
  49. package/lib/execution/builder/index.js.map +1 -1
  50. package/lib/metrics/metrics/lodestar.d.ts +1 -1
  51. package/lib/metrics/metrics/lodestar.d.ts.map +1 -1
  52. package/lib/metrics/metrics/lodestar.js +4 -3
  53. package/lib/metrics/metrics/lodestar.js.map +1 -1
  54. package/lib/network/interface.d.ts +1 -0
  55. package/lib/network/interface.d.ts.map +1 -1
  56. package/lib/network/network.d.ts +1 -0
  57. package/lib/network/network.d.ts.map +1 -1
  58. package/lib/network/network.js +5 -0
  59. package/lib/network/network.js.map +1 -1
  60. package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
  61. package/lib/network/processor/gossipHandlers.js +12 -4
  62. package/lib/network/processor/gossipHandlers.js.map +1 -1
  63. package/package.json +14 -14
  64. package/src/api/impl/beacon/blocks/index.ts +36 -0
  65. package/src/api/impl/beacon/pool/index.ts +2 -1
  66. package/src/api/impl/validator/index.ts +90 -38
  67. package/src/chain/blocks/importBlock.ts +2 -1
  68. package/src/chain/blocks/importExecutionPayload.ts +7 -1
  69. package/src/chain/chain.ts +2 -0
  70. package/src/chain/errors/executionPayloadBid.ts +4 -1
  71. package/src/chain/forkChoice/index.ts +14 -4
  72. package/src/chain/opPools/executionPayloadBidPool.ts +10 -9
  73. package/src/chain/prepareNextSlot.ts +2 -1
  74. package/src/chain/produceBlock/produceBlockBody.ts +108 -34
  75. package/src/chain/validation/executionPayloadBid.ts +32 -14
  76. package/src/chain/validatorMonitor.ts +18 -0
  77. package/src/execution/builder/index.ts +1 -4
  78. package/src/metrics/metrics/lodestar.ts +4 -3
  79. package/src/network/interface.ts +1 -0
  80. package/src/network/network.ts +11 -0
  81. package/src/network/processor/gossipHandlers.ts +16 -4
  82. package/lib/execution/builder/utils.d.ts +0 -5
  83. package/lib/execution/builder/utils.d.ts.map +0 -1
  84. package/lib/execution/builder/utils.js +0 -17
  85. package/lib/execution/builder/utils.js.map +0 -1
  86. package/src/execution/builder/utils.ts +0 -19
@@ -237,6 +237,7 @@ export async function importExecutionPayload(
237
237
  blockRootHex,
238
238
  blockHashHex,
239
239
  envelope.payload.blockNumber,
240
+ envelope.payload.gasLimit,
240
241
  execStatus,
241
242
  dataAvailabilityStatus
242
243
  );
@@ -254,7 +255,11 @@ export async function importExecutionPayload(
254
255
  }
255
256
 
256
257
  // 8. Record metrics for payload envelope and column sources
257
- this.metrics?.importPayload.bySource.inc({source: payloadInput.getPayloadEnvelopeSource().source});
258
+ const delaySec = this.clock.secFromSlot(slot);
259
+ this.metrics?.importPayload.elapsedTimeTillImported.observe(
260
+ {source: payloadInput.getPayloadEnvelopeSource().source},
261
+ delaySec
262
+ );
258
263
  for (const {source} of payloadInput.getSampledColumnsWithSource()) {
259
264
  this.metrics?.importPayload.columnsBySource.inc({source});
260
265
  }
@@ -275,6 +280,7 @@ export async function importExecutionPayload(
275
280
  builderIndex: envelope.builderIndex,
276
281
  blockRoot: blockRootHex,
277
282
  blockHash: blockHashHex,
283
+ delaySec,
278
284
  });
279
285
  }
280
286
 
@@ -1049,6 +1049,7 @@ export class BeaconChain implements IBeaconChain {
1049
1049
  feeRecipient,
1050
1050
  commonBlockBodyPromise,
1051
1051
  parentBlock,
1052
+ builderBid,
1052
1053
  }: BlockAttributes & {commonBlockBodyPromise: Promise<CommonBlockBody>}
1053
1054
  ): Promise<{
1054
1055
  block: AssembledBlockType<T>;
@@ -1078,6 +1079,7 @@ export class BeaconChain implements IBeaconChain {
1078
1079
  proposerIndex,
1079
1080
  proposerPubKey,
1080
1081
  commonBlockBodyPromise,
1082
+ builderBid,
1081
1083
  }
1082
1084
  );
1083
1085
 
@@ -9,6 +9,7 @@ export enum ExecutionPayloadBidErrorCode {
9
9
  BID_TOO_HIGH = "EXECUTION_PAYLOAD_BID_ERROR_BID_TOO_HIGH",
10
10
  TOO_MANY_KZG_COMMITMENTS = "EXECUTION_PAYLOAD_BID_ERROR_TOO_MANY_KZG_COMMITMENTS",
11
11
  UNKNOWN_BLOCK_ROOT = "EXECUTION_PAYLOAD_BID_ERROR_UNKNOWN_BLOCK_ROOT",
12
+ UNKNOWN_PARENT_BLOCK_HASH = "EXECUTION_PAYLOAD_BID_ERROR_UNKNOWN_PARENT_BLOCK_HASH",
12
13
  INVALID_SLOT = "EXECUTION_PAYLOAD_BID_ERROR_INVALID_SLOT",
13
14
  INVALID_SIGNATURE = "EXECUTION_PAYLOAD_BID_ERROR_INVALID_SIGNATURE",
14
15
  NO_MATCHING_PROPOSER_PREFERENCES = "EXECUTION_PAYLOAD_BID_ERROR_NO_MATCHING_PROPOSER_PREFERENCES",
@@ -38,6 +39,7 @@ export type ExecutionPayloadBidErrorType =
38
39
  commitmentLimit: number;
39
40
  }
40
41
  | {code: ExecutionPayloadBidErrorCode.UNKNOWN_BLOCK_ROOT; parentBlockRoot: RootHex}
42
+ | {code: ExecutionPayloadBidErrorCode.UNKNOWN_PARENT_BLOCK_HASH; parentBlockHash: RootHex}
41
43
  | {code: ExecutionPayloadBidErrorCode.INVALID_SLOT; builderIndex: BuilderIndex; slot: Slot}
42
44
  | {code: ExecutionPayloadBidErrorCode.INVALID_SIGNATURE; builderIndex: BuilderIndex; slot: Slot}
43
45
  | {
@@ -56,7 +58,8 @@ export type ExecutionPayloadBidErrorType =
56
58
  code: ExecutionPayloadBidErrorCode.PROPOSER_PREFERENCES_GAS_LIMIT_MISMATCH;
57
59
  builderIndex: BuilderIndex;
58
60
  bidGasLimit: number;
59
- expectedGasLimit: number;
61
+ parentGasLimit: number;
62
+ targetGasLimit: number;
60
63
  };
61
64
 
62
65
  export class ExecutionPayloadBidError extends GossipActionError<ExecutionPayloadBidErrorType> {}
@@ -140,9 +140,11 @@ export function initializeForkChoiceFromFinalizedState(
140
140
  executionPayloadBlockHash: isStatePostGloas(state)
141
141
  ? toRootHex(state.latestBlockHash)
142
142
  : toRootHex(state.latestExecutionPayloadHeader.blockHash),
143
- // TODO GLOAS: executionPayloadNumber is not tracked in BeaconState post-gloas (EIP-7732 removed
144
- // latestExecutionPayloadHeader). Using 0 as unavailable fallback until a solution is found.
143
+ // TODO GLOAS: executionPayloadNumber/GasLimit are not tracked in BeaconState post-gloas
144
+ // (EIP-7732 removed latestExecutionPayloadHeader). Using 0 as unavailable fallback
145
+ // see initializeForkChoiceFromUnfinalizedState for the same caveat on validation.
145
146
  executionPayloadNumber: isStatePostGloas(state) ? 0 : state.payloadBlockNumber,
147
+ executionPayloadGasLimit: isStatePostGloas(state) ? 0 : state.latestExecutionPayloadHeader.gasLimit,
146
148
  executionStatus: blockHeader.slot === GENESIS_SLOT ? ExecutionStatus.Valid : ExecutionStatus.Syncing,
147
149
  }
148
150
  : {executionPayloadBlockHash: null, executionStatus: ExecutionStatus.PreMerge}),
@@ -232,9 +234,17 @@ export function initializeForkChoiceFromUnfinalizedState(
232
234
  executionPayloadBlockHash: isStatePostGloas(unfinalizedState)
233
235
  ? toRootHex(unfinalizedState.latestBlockHash)
234
236
  : toRootHex(unfinalizedState.latestExecutionPayloadHeader.blockHash),
235
- // TODO GLOAS: executionPayloadNumber is not tracked in BeaconState post-gloas (EIP-7732 removed
236
- // latestExecutionPayloadHeader). Using 0 as unavailable fallback until a solution is found.
237
+ // TODO GLOAS: executionPayloadNumber/GasLimit are not tracked in BeaconState post-gloas
238
+ // (EIP-7732 removed latestExecutionPayloadHeader). Using 0 as unavailable fallback until
239
+ // a solution is found. The 0 doesn't gate validation in practice: at boot the head's
240
+ // PENDING variant's `executionPayloadBlockHash` is the *parent's* payload hash (per the
241
+ // PENDING/EMPTY convention), so gossip bids that reference the head's *own* payload
242
+ // hash won't match this variant anyway and will IGNORE until `onExecutionPayload`
243
+ // upgrades the head to FULL with real values.
237
244
  executionPayloadNumber: isStatePostGloas(unfinalizedState) ? 0 : unfinalizedState.payloadBlockNumber,
245
+ executionPayloadGasLimit: isStatePostGloas(unfinalizedState)
246
+ ? 0
247
+ : unfinalizedState.latestExecutionPayloadHeader.gasLimit,
238
248
  executionStatus: blockHeader.slot === GENESIS_SLOT ? ExecutionStatus.Valid : ExecutionStatus.Syncing,
239
249
  }
240
250
  : {executionPayloadBlockHash: null, executionStatus: ExecutionStatus.PreMerge}),
@@ -12,13 +12,13 @@ type BlockRootHex = string;
12
12
  type BlockHashHex = string;
13
13
 
14
14
  /**
15
- * Store the best execution payload bid per slot / (parent block root, parent block hash).
15
+ * Store the best signed execution payload bid per slot / (parent block root, parent block hash).
16
16
  */
17
17
  export class ExecutionPayloadBidPool {
18
18
  private readonly bidByParentHashByParentRootBySlot = new MapDef<
19
19
  Slot,
20
- MapDef<BlockRootHex, Map<BlockHashHex, gloas.ExecutionPayloadBid>>
21
- >(() => new MapDef<BlockRootHex, Map<BlockHashHex, gloas.ExecutionPayloadBid>>(() => new Map()));
20
+ MapDef<BlockRootHex, Map<BlockHashHex, gloas.SignedExecutionPayloadBid>>
21
+ >(() => new MapDef<BlockRootHex, Map<BlockHashHex, gloas.SignedExecutionPayloadBid>>(() => new Map()));
22
22
  private lowestPermissibleSlot = 0;
23
23
 
24
24
  get size(): number {
@@ -31,8 +31,8 @@ export class ExecutionPayloadBidPool {
31
31
  return count;
32
32
  }
33
33
 
34
- add(bid: gloas.ExecutionPayloadBid): InsertOutcome {
35
- const {slot, parentBlockRoot, parentBlockHash, value} = bid;
34
+ add(bid: gloas.SignedExecutionPayloadBid): InsertOutcome {
35
+ const {slot, parentBlockRoot, parentBlockHash, value} = bid.message;
36
36
  const lowestPermissibleSlot = this.lowestPermissibleSlot;
37
37
 
38
38
  if (slot < lowestPermissibleSlot) {
@@ -45,7 +45,7 @@ export class ExecutionPayloadBidPool {
45
45
  const existing = bidByParentHash.get(parentHashHex);
46
46
 
47
47
  if (existing) {
48
- const existingValue = existing.value;
48
+ const existingValue = existing.message.value;
49
49
  const newValue = value;
50
50
  if (newValue > existingValue) {
51
51
  bidByParentHash.set(parentHashHex, bid);
@@ -59,14 +59,15 @@ export class ExecutionPayloadBidPool {
59
59
  }
60
60
 
61
61
  /**
62
- * Return the highest-value bid matching slot, parent block hash, and parent block root.
62
+ * Return the highest-value signed bid matching slot, parent block hash, and parent block root.
63
63
  * Used for gossip validation and block production.
64
64
  */
65
65
  getBestBid(
66
66
  slot: Slot,
67
- parentBlockHash: BlockHashHex,
67
+ parentBlockHash: BlockHashHex | null,
68
68
  parentBlockRoot: BlockRootHex
69
- ): gloas.ExecutionPayloadBid | null {
69
+ ): gloas.SignedExecutionPayloadBid | null {
70
+ if (parentBlockHash === null) return null;
70
71
  const bidByParentHash = this.bidByParentHashByParentRootBySlot.get(slot)?.get(parentBlockRoot);
71
72
  return bidByParentHash?.get(parentBlockHash) ?? null;
72
73
  }
@@ -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)) {
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,9 +18,9 @@ import {
18
18
  G2_POINT_AT_INFINITY,
19
19
  IBeaconStateView,
20
20
  type IBeaconStateViewBellatrix,
21
- type IBeaconStateViewGloas,
22
21
  computeEpochAtSlot,
23
22
  computeTimeAtSlot,
23
+ getExpectedGasLimit,
24
24
  isStatePostBellatrix,
25
25
  isStatePostCapella,
26
26
  isStatePostGloas,
@@ -50,16 +50,10 @@ import {
50
50
  gloas,
51
51
  ssz,
52
52
  } from "@lodestar/types";
53
- 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";
54
54
  import {ZERO_HASH_HEX} from "../../constants/index.js";
55
55
  import {numToQuantity} from "../../execution/engine/utils.js";
56
- import {
57
- IExecutionBuilder,
58
- IExecutionEngine,
59
- PayloadAttributes,
60
- PayloadId,
61
- getExpectedGasLimit,
62
- } from "../../execution/index.js";
56
+ import {IExecutionBuilder, IExecutionEngine, PayloadAttributes, PayloadId} from "../../execution/index.js";
63
57
  import {getShufflingDependentRoot} from "../../util/dependentRoot.js";
64
58
  import {fromGraffitiBytes} from "../../util/graffiti.js";
65
59
  import {kzg} from "../../util/kzg.js";
@@ -97,6 +91,8 @@ export type BlockAttributes = {
97
91
  slot: Slot;
98
92
  parentBlock: ProtoBlock;
99
93
  feeRecipient?: string;
94
+ /** When provided, build block with this builder bid instead of a self-build bid */
95
+ builderBid?: gloas.SignedExecutionPayloadBid;
100
96
  };
101
97
 
102
98
  export enum BlockType {
@@ -156,6 +152,28 @@ export type ProduceResult =
156
152
  | ProduceFullPhase0
157
153
  | ProduceBlinded;
158
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
+
159
177
  export async function produceBlockBody<T extends BlockType>(
160
178
  this: BeaconChain,
161
179
  blockType: T,
@@ -178,6 +196,7 @@ export async function produceBlockBody<T extends BlockType>(
178
196
  proposerIndex,
179
197
  proposerPubKey,
180
198
  commonBlockBodyPromise,
199
+ builderBid,
181
200
  } = blockAttr;
182
201
  let executionPayloadValue: Wei;
183
202
  let blockBody: AssembledBodyType<T>;
@@ -198,7 +217,43 @@ export async function produceBlockBody<T extends BlockType>(
198
217
  };
199
218
  this.logger.verbose("Producing beacon block body", logMeta);
200
219
 
201
- 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)) {
202
257
  if (!isStatePostGloas(currentState)) {
203
258
  throw new Error("Expected Gloas state for Gloas block production");
204
259
  }
@@ -215,19 +270,16 @@ export async function produceBlockBody<T extends BlockType>(
215
270
 
216
271
  const endExecutionPayload = this.metrics?.executionBlockProductionTimeSteps.startTimer();
217
272
 
218
- this.logger.verbose("Preparing execution payload from engine", {
219
- slot: blockSlot,
220
- parentBlockRoot: toRootHex(parentBlockRoot),
221
- feeRecipient,
222
- });
223
-
224
273
  // Get execution payload from EL
225
274
  let parentBlockHash: Bytes32;
226
275
  let parentExecutionRequests: electra.ExecutionRequests;
227
276
  // Apply parent payload once here as it's reused by EL prep and voluntary exit filtering below
228
277
  let stateAfterParentPayload: IBeaconStateViewBellatrix = currentState;
229
- const isExtendingPayload = this.forkChoice.shouldExtendPayload(toRootHex(parentBlockRoot));
230
- 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
280
+ // signalled the blob data is not available, forcing a build on EMPTY (reorg).
281
+ const isBuildingOnFull = this.forkChoice.shouldBuildOnFull(parentBlock);
282
+ if (isBuildingOnFull) {
231
283
  parentBlockHash = currentState.latestExecutionPayloadBid.blockHash;
232
284
  parentExecutionRequests = await this.getParentExecutionRequests(parentBlock.slot, parentBlock.blockRoot);
233
285
  stateAfterParentPayload = currentState.withParentPayloadApplied(parentExecutionRequests);
@@ -250,6 +302,16 @@ export async function produceBlockBody<T extends BlockType>(
250
302
  const {prepType, payloadId} = prepareRes;
251
303
  Object.assign(logMeta, {executionPayloadPrepType: prepType});
252
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
+
253
315
  if (prepType !== PayloadPreparationType.Cached) {
254
316
  await sleep(PAYLOAD_GENERATION_TIME_MS);
255
317
  }
@@ -303,14 +365,11 @@ export async function produceBlockBody<T extends BlockType>(
303
365
  blockSlot - 1
304
366
  );
305
367
  gloasBody.parentExecutionRequests = parentExecutionRequests;
306
- // Drop voluntary exits that parent_execution_requests have invalidated (e.g. a withdrawal
307
- // request initiating an exit on the same validator). Op pool selected against the unapplied
308
- // state, so re-validate against the post-apply state to avoid producing an invalid block.
309
- if (isExtendingPayload && commonBlockBody.voluntaryExits.length > 0) {
310
- gloasBody.voluntaryExits = commonBlockBody.voluntaryExits.filter((signedVoluntaryExit) =>
311
- stateAfterParentPayload.isValidVoluntaryExit(signedVoluntaryExit, false)
312
- );
313
- }
368
+ gloasBody.voluntaryExits = maybeFilterInvalidatedVoluntaryExits(
369
+ commonBlockBody,
370
+ isBuildingOnFull,
371
+ () => stateAfterParentPayload
372
+ );
314
373
  blockBody = gloasBody as AssembledBodyType<T>;
315
374
 
316
375
  // Store execution payload data required to construct execution payload envelope later
@@ -331,6 +390,7 @@ export async function produceBlockBody<T extends BlockType>(
331
390
  fetchedTime,
332
391
  executionBlockHash: toRootHex(executionPayload.blockHash),
333
392
  blobs: blobsBundle.commitments.length,
393
+ gasLimit: executionPayload.gasLimit,
334
394
  });
335
395
 
336
396
  Object.assign(logMeta, {
@@ -869,9 +929,9 @@ function preparePayloadAttributes(
869
929
  (payloadAttributes as gloas.SSEPayloadAttributes["payloadAttributes"]).slotNumber = prepareSlot;
870
930
  (payloadAttributes as gloas.SSEPayloadAttributes["payloadAttributes"]).targetGasLimit = getProposerTargetGasLimit(
871
931
  chain,
872
- prepareState,
873
932
  prepareSlot,
874
- parentBlockRoot
933
+ parentBlockRoot,
934
+ parentBlockHash
875
935
  );
876
936
  }
877
937
 
@@ -886,14 +946,20 @@ function preparePayloadAttributes(
886
946
  * (same `(slot, dependent_root)` lookup as gossip bid validation). When no matching
887
947
  * preferences are pooled, target the parent payload's gas limit so the gas limit stays
888
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).
889
954
  */
890
955
  function getProposerTargetGasLimit(
891
956
  chain: {forkChoice: IForkChoice; proposerPreferencesPool: ProposerPreferencesPool},
892
- state: IBeaconStateViewGloas,
893
957
  prepareSlot: Slot,
894
- parentBlockRoot: Root
958
+ parentBlockRoot: Root,
959
+ parentBlockHash: Bytes32
895
960
  ): number {
896
- const parentBlock = chain.forkChoice.getBlockHexDefaultStatus(toRootHex(parentBlockRoot));
961
+ const parentBlockRootHex = toRootHex(parentBlockRoot);
962
+ const parentBlock = chain.forkChoice.getBlockHexDefaultStatus(parentBlockRootHex);
897
963
  const dependentRootHex = (() => {
898
964
  if (parentBlock === null) {
899
965
  return null;
@@ -911,9 +977,17 @@ function getProposerTargetGasLimit(
911
977
  })();
912
978
 
913
979
  const pref = dependentRootHex !== null ? chain.proposerPreferencesPool.get(prepareSlot, dependentRootHex) : null;
914
- // TODO GLOAS: state.latestExecutionPayloadBid is the latest *bid*, not the latest *executed*
915
- // payload — for EMPTY parents this drifts. Consider having a default value like Prysm's DefaultBuilderGasLimit.
916
- return Number(pref ? pref.message.targetGasLimit : state.latestExecutionPayloadBid.gasLimit);
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;
917
991
  }
918
992
 
919
993
  export async function produceCommonBlockBody<T extends BlockType>(
@@ -4,9 +4,10 @@ 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 {ValidatorIndex, gloas} from "@lodestar/types";
10
11
  import {byteArrayEquals, toHex, toRootHex} from "@lodestar/utils";
11
12
  import {getShufflingDependentRoot} from "../../util/dependentRoot.js";
12
13
  import {ExecutionPayloadBidError, ExecutionPayloadBidErrorCode, GossipAction} from "../errors/index.js";
@@ -16,21 +17,21 @@ import {RegenCaller} from "../regen/index.js";
16
17
  export async function validateApiExecutionPayloadBid(
17
18
  chain: IBeaconChain,
18
19
  signedExecutionPayloadBid: gloas.SignedExecutionPayloadBid
19
- ): Promise<void> {
20
+ ): Promise<{proposerIndex: ValidatorIndex}> {
20
21
  return validateExecutionPayloadBid(chain, signedExecutionPayloadBid);
21
22
  }
22
23
 
23
24
  export async function validateGossipExecutionPayloadBid(
24
25
  chain: IBeaconChain,
25
26
  signedExecutionPayloadBid: gloas.SignedExecutionPayloadBid
26
- ): Promise<void> {
27
+ ): Promise<{proposerIndex: ValidatorIndex}> {
27
28
  return validateExecutionPayloadBid(chain, signedExecutionPayloadBid);
28
29
  }
29
30
 
30
31
  async function validateExecutionPayloadBid(
31
32
  chain: IBeaconChain,
32
33
  signedExecutionPayloadBid: gloas.SignedExecutionPayloadBid
33
- ): Promise<void> {
34
+ ): Promise<{proposerIndex: ValidatorIndex}> {
34
35
  const bid = signedExecutionPayloadBid.message;
35
36
  const parentBlockRootHex = toRootHex(bid.parentBlockRoot);
36
37
  const parentBlockHashHex = toRootHex(bid.parentBlockHash);
@@ -128,14 +129,33 @@ async function validateExecutionPayloadBid(
128
129
  });
129
130
  }
130
131
 
131
- // [REJECT] `bid.gas_limit == proposer_preferences.target_gas_limit`.
132
+ // [IGNORE] `bid.parent_block_hash` is the block hash of a known execution payload in fork
133
+ // choice. Looks up the variant of `bid.parent_block_root` whose payload hash matches
134
+ // `bid.parent_block_hash` — works for both FULL parents (FULL variant carries the delivered
135
+ // payload's hash) and EMPTY parents (EMPTY/PENDING variants carry the inherited parent
136
+ // payload's hash, since the new block doesn't have its own payload). Variant carries the
137
+ // executed payload's gas_limit, which we use as `parent_gas_limit` below.
138
+ const parentPayloadVariant = chain.forkChoice.getBlockHexAndBlockHash(parentBlockRootHex, parentBlockHashHex);
139
+ if (parentPayloadVariant === null || parentPayloadVariant.executionPayloadBlockHash === null) {
140
+ throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
141
+ code: ExecutionPayloadBidErrorCode.UNKNOWN_PARENT_BLOCK_HASH,
142
+ parentBlockHash: parentBlockHashHex,
143
+ });
144
+ }
145
+
146
+ // [IGNORE] `is_gas_limit_target_compatible(parent_gas_limit, bid.gas_limit, target_gas_limit)`,
147
+ // where `parent_gas_limit` is the `gas_limit` of the parent execution payload and
148
+ // `target_gas_limit` is `proposer_preferences.target_gas_limit`.
132
149
  const bidGasLimit = Number(bid.gasLimit);
133
- if (bidGasLimit !== proposerPreferences.message.targetGasLimit) {
134
- throw new ExecutionPayloadBidError(GossipAction.REJECT, {
150
+ const parentGasLimit = parentPayloadVariant.executionPayloadGasLimit;
151
+ const targetGasLimit = proposerPreferences.message.targetGasLimit;
152
+ if (!isGasLimitTargetCompatible(parentGasLimit, bidGasLimit, targetGasLimit)) {
153
+ throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
135
154
  code: ExecutionPayloadBidErrorCode.PROPOSER_PREFERENCES_GAS_LIMIT_MISMATCH,
136
155
  builderIndex: bid.builderIndex,
137
156
  bidGasLimit,
138
- expectedGasLimit: proposerPreferences.message.targetGasLimit,
157
+ parentGasLimit,
158
+ targetGasLimit,
139
159
  });
140
160
  }
141
161
 
@@ -166,11 +186,11 @@ async function validateExecutionPayloadBid(
166
186
  // [IGNORE] this bid is the highest value bid seen for the tuple
167
187
  // `(bid.slot, bid.parent_block_hash, bid.parent_block_root)`.
168
188
  const bestBid = chain.executionPayloadBidPool.getBestBid(bid.slot, parentBlockHashHex, parentBlockRootHex);
169
- if (bestBid !== null && bestBid.value >= bid.value) {
189
+ if (bestBid !== null && bestBid.message.value >= bid.value) {
170
190
  throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
171
191
  code: ExecutionPayloadBidErrorCode.BID_TOO_LOW,
172
192
  bidValue: bid.value,
173
- currentHighestBid: bestBid.value,
193
+ currentHighestBid: bestBid.message.value,
174
194
  });
175
195
  }
176
196
  // [IGNORE] `bid.value` is less or equal than the builder's excess balance --
@@ -183,10 +203,6 @@ async function validateExecutionPayloadBid(
183
203
  });
184
204
  }
185
205
 
186
- // [IGNORE] `bid.parent_block_hash` is the block hash of a known execution
187
- // payload in fork choice.
188
- // TODO GLOAS: implement this
189
-
190
206
  // [REJECT] `signed_execution_payload_bid.signature` is valid with respect to the `bid.builder_index`.
191
207
  const signatureSet = createSingleSignatureSetFromComponents(
192
208
  PublicKey.fromBytes(builder.pubkey),
@@ -204,4 +220,6 @@ async function validateExecutionPayloadBid(
204
220
 
205
221
  // Valid
206
222
  chain.seenExecutionPayloadBids.add(bid.slot, bid.builderIndex);
223
+
224
+ return {proposerIndex: proposerPreferences.message.validatorIndex};
207
225
  }
@@ -66,6 +66,7 @@ export type ValidatorMonitor = {
66
66
  delaySec: Seconds,
67
67
  envelope: gloas.SignedExecutionPayloadEnvelope
68
68
  ): void;
69
+ registerExecutionPayloadBid(src: OpSource, proposerIndex: ValidatorIndex, bid: gloas.ExecutionPayloadBid): void;
69
70
  registerImportedBlock(block: BeaconBlock, data: {proposerBalanceDelta: number}): void;
70
71
  onPoolSubmitUnaggregatedAttestation(
71
72
  seenTimestampSec: number,
@@ -459,6 +460,23 @@ export function createValidatorMonitor(
459
460
  // TODO GLOAS: implement execution payload envelope monitoring
460
461
  },
461
462
 
463
+ registerExecutionPayloadBid(src, proposerIndex, bid) {
464
+ if (!validators.has(proposerIndex)) {
465
+ return;
466
+ }
467
+ log("Received an execution payload bid for monitored proposer", {
468
+ slot: bid.slot,
469
+ proposerIndex,
470
+ src,
471
+ builderIndex: bid.builderIndex,
472
+ gasLimit: bid.gasLimit,
473
+ value: bid.value.toString(),
474
+ parentBlockRoot: toRootHex(bid.parentBlockRoot),
475
+ parentBlockHash: toRootHex(bid.parentBlockHash),
476
+ blockHash: toRootHex(bid.blockHash),
477
+ });
478
+ },
479
+
462
480
  registerImportedBlock(block, {proposerBalanceDelta}) {
463
481
  const validator = validators.get(block.proposerIndex);
464
482
  if (validator) {
@@ -1,11 +1,8 @@
1
1
  import {ChainForkConfig} from "@lodestar/config";
2
2
  import {Logger} from "@lodestar/logger";
3
3
  import {Metrics} from "../../metrics/metrics.js";
4
- import {IExecutionBuilder} from "./interface.js";
5
-
6
- export {getExpectedGasLimit} from "./utils.js";
7
-
8
4
  import {ExecutionBuilderHttp, ExecutionBuilderHttpOpts, defaultExecutionBuilderHttpOpts} from "./http.js";
5
+ import {IExecutionBuilder} from "./interface.js";
9
6
 
10
7
  export {ExecutionBuilderHttp, defaultExecutionBuilderHttpOpts};
11
8
 
@@ -987,10 +987,11 @@ export function createLodestarMetrics(
987
987
  }),
988
988
  },
989
989
  importPayload: {
990
- bySource: register.gauge<{source: PayloadEnvelopeInputSource}>({
991
- name: "lodestar_import_payload_by_source_total",
992
- help: "Total number of imported execution payload envelopes by source",
990
+ elapsedTimeTillImported: register.histogram<{source: PayloadEnvelopeInputSource}>({
991
+ name: "lodestar_import_payload_elapsed_time_till_imported_seconds",
992
+ help: "Time elapsed between slot time and the time execution payload envelope is imported (added to fork choice)",
993
993
  labelNames: ["source"],
994
+ buckets: [1, 2, 3, 6, 9, 12],
994
995
  }),
995
996
  columnsBySource: register.gauge<{source: PayloadEnvelopeInputSource}>({
996
997
  name: "lodestar_import_payload_columns_by_source_total",
@@ -113,6 +113,7 @@ export interface INetwork extends INetworkCorePublic {
113
113
  publishLightClientFinalityUpdate(update: LightClientFinalityUpdate): Promise<number>;
114
114
  publishLightClientOptimisticUpdate(update: LightClientOptimisticUpdate): Promise<number>;
115
115
  publishSignedExecutionPayloadEnvelope(signedEnvelope: gloas.SignedExecutionPayloadEnvelope): Promise<number>;
116
+ publishSignedExecutionPayloadBid(signedBid: gloas.SignedExecutionPayloadBid): Promise<number>;
116
117
  publishPayloadAttestationMessage(payloadAttestationMessage: gloas.PayloadAttestationMessage): Promise<number>;
117
118
  publishProposerPreferences(signedProposerPreferences: gloas.SignedProposerPreferences): Promise<number>;
118
119
 
@@ -515,6 +515,17 @@ export class Network implements INetwork {
515
515
  );
516
516
  }
517
517
 
518
+ async publishSignedExecutionPayloadBid(signedBid: gloas.SignedExecutionPayloadBid): Promise<number> {
519
+ const epoch = computeEpochAtSlot(signedBid.message.slot);
520
+ const boundary = this.config.getForkBoundaryAtEpoch(epoch);
521
+
522
+ return this.publishGossip<GossipType.execution_payload_bid>(
523
+ {type: GossipType.execution_payload_bid, boundary},
524
+ signedBid,
525
+ {ignoreDuplicatePublishError: true}
526
+ );
527
+ }
528
+
518
529
  async publishPayloadAttestationMessage(payloadAttestationMessage: gloas.PayloadAttestationMessage): Promise<number> {
519
530
  const epoch = computeEpochAtSlot(payloadAttestationMessage.data.slot);
520
531
  const boundary = this.config.getForkBoundaryAtEpoch(epoch);