@lodestar/beacon-node 1.48.0-dev.57c1e5614b → 1.48.0-dev.58bd2c7c96

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.
@@ -133,37 +133,40 @@ type BidCandidate = {
133
133
  };
134
134
 
135
135
  /**
136
- * Ranking value of a bid, the boosted total payment scaled by 100 as `boostFactor` is a percentage.
137
- * Kept scaled so bids that differ by less than a percent are not truncated into a tie.
136
+ * Return the best bid by boosted total payment. A candidate with the max boost factor is
137
+ * preferred over any other regardless of value, ties prefer a builder api bid over the
138
+ * p2p bid, and the earlier received bid between builder api bids.
138
139
  */
139
- function getBoostedTotalScaled({totalGwei, boostFactor}: BidCandidate): bigint {
140
- return boostFactor * totalGwei;
141
- }
142
-
143
- /** Order bid candidates by preference, best first, used to select a bid and to log the ranking */
144
- function compareBidCandidates(a: BidCandidate, b: BidCandidate): number {
145
- // Preserve max boost preference before comparing bid values
146
- const aIsMaxBoost = a.boostFactor === MAX_BUILDER_BOOST_FACTOR;
147
- const bIsMaxBoost = b.boostFactor === MAX_BUILDER_BOOST_FACTOR;
148
- if (aIsMaxBoost !== bIsMaxBoost) {
149
- return aIsMaxBoost ? -1 : 1;
150
- }
151
-
152
- const aBoostedTotal = getBoostedTotalScaled(a);
153
- const bBoostedTotal = getBoostedTotalScaled(b);
154
- if (aBoostedTotal !== bBoostedTotal) {
155
- return aBoostedTotal > bBoostedTotal ? -1 : 1;
156
- }
157
-
158
- // A tie prefers a builder api bid over the p2p bid
159
- const aIsBuilderApi = a.url !== undefined;
160
- const bIsBuilderApi = b.url !== undefined;
161
- if (aIsBuilderApi !== bIsBuilderApi) {
162
- return aIsBuilderApi ? -1 : 1;
140
+ function selectBestBid(candidates: BidCandidate[]): BidCandidate | null {
141
+ const boostedValue = ({totalGwei, boostFactor}: BidCandidate): bigint => boostFactor * totalGwei;
142
+ let best: BidCandidate | null = null;
143
+ for (const candidate of candidates) {
144
+ if (best === null) {
145
+ best = candidate;
146
+ continue;
147
+ }
148
+ // Preserve max boost preference before comparing bid values
149
+ const candidateIsMaxBoost = candidate.boostFactor === MAX_BUILDER_BOOST_FACTOR;
150
+ const bestIsMaxBoost = best.boostFactor === MAX_BUILDER_BOOST_FACTOR;
151
+ if (candidateIsMaxBoost !== bestIsMaxBoost) {
152
+ if (candidateIsMaxBoost) {
153
+ best = candidate;
154
+ }
155
+ continue;
156
+ }
157
+ const candidateValue = boostedValue(candidate);
158
+ const bestValue = boostedValue(best);
159
+ if (candidateValue > bestValue) {
160
+ best = candidate;
161
+ } else if (
162
+ candidateValue === bestValue &&
163
+ // A tie prefers a builder api bid over the p2p bid, and the earlier received bid otherwise
164
+ (best.url === undefined || (candidate.url !== undefined && candidate.receivedMs < best.receivedMs))
165
+ ) {
166
+ best = candidate;
167
+ }
163
168
  }
164
-
165
- // and the earlier received bid between builder api bids
166
- return a.receivedMs - b.receivedMs;
169
+ return best;
167
170
  }
168
171
 
169
172
  type ProduceBlockContentsRes = {executionPayloadValue: Wei; consensusBlockValue: Wei} & {
@@ -1046,24 +1049,20 @@ export function getValidatorApi(
1046
1049
  });
1047
1050
  }
1048
1051
 
1049
- if (candidates.length === 0) {
1050
- return null;
1051
- }
1052
-
1053
- const rankedCandidates = candidates.toSorted(compareBidCandidates);
1054
- for (const [index, candidate] of rankedCandidates.entries()) {
1055
- logger.debug("Builder bid candidate", {
1052
+ const best = selectBestBid(candidates);
1053
+ if (candidates.length > 0) {
1054
+ logger.debug("Ranked builder bid candidates", {
1056
1055
  slot,
1057
- rank: index + 1,
1058
- source: candidate.url !== undefined ? toPrintableUrl(candidate.url) : "p2p",
1059
- builder: candidate.signedBid.message.builderIndex,
1060
- total: prettyGweiToEth(candidate.totalGwei),
1061
- boost: candidate.boostFactor,
1062
- boosted: prettyGweiToEth(getBoostedTotalScaled(candidate) / 100n),
1063
- receivedMs: candidate.receivedMs,
1056
+ candidates: candidates
1057
+ .map(
1058
+ (candidate) =>
1059
+ `${candidate.url ?? "p2p"}:total=${prettyGweiToEth(candidate.totalGwei)}:boost=${candidate.boostFactor}:received=${candidate.receivedMs}ms`
1060
+ )
1061
+ .join(","),
1062
+ bidSource: best?.url ?? "p2p",
1064
1063
  });
1065
1064
  }
1066
- return rankedCandidates[0];
1065
+ return best;
1067
1066
  })();
1068
1067
 
1069
1068
  const commonBlockBodyPromise = chain.produceCommonBlockBody({
@@ -78,8 +78,8 @@ export enum BlockErrorCode {
78
78
  TOO_MANY_KZG_COMMITMENTS = "BLOCK_ERROR_TOO_MANY_KZG_COMMITMENTS",
79
79
  /** Bid parent block root does not match block parent root */
80
80
  BID_PARENT_ROOT_MISMATCH = "BLOCK_ERROR_BID_PARENT_ROOT_MISMATCH",
81
- /** A block contains deposits where none are allowed */
82
- NON_ZERO_DEPOSITS = "BLOCK_ERROR_NON_ZERO_DEPOSITS",
81
+ /** A block body operation or parent execution request list exceeds its per-block limit */
82
+ TOO_MANY_BLOCK_OPERATIONS = "BLOCK_ERROR_TOO_MANY_BLOCK_OPERATIONS",
83
83
  /** The parent block's execution payload has been verified as invalid */
84
84
  PARENT_EXECUTION_INVALID = "BLOCK_ERROR_PARENT_EXECUTION_INVALID",
85
85
  /**
@@ -153,7 +153,7 @@ export type BlockErrorType =
153
153
  | {code: BlockErrorCode.DATA_UNAVAILABLE}
154
154
  | {code: BlockErrorCode.TOO_MANY_KZG_COMMITMENTS; blobKzgCommitmentsLen: number; commitmentLimit: number}
155
155
  | {code: BlockErrorCode.BID_PARENT_ROOT_MISMATCH; bidParentRoot: RootHex; blockParentRoot: RootHex}
156
- | {code: BlockErrorCode.NON_ZERO_DEPOSITS; count: number}
156
+ | {code: BlockErrorCode.TOO_MANY_BLOCK_OPERATIONS; name: string; count: number; limit: number}
157
157
  | {code: BlockErrorCode.PARENT_EXECUTION_INVALID; parentRoot: RootHex}
158
158
  | {code: BlockErrorCode.PARENT_PAYLOAD_UNKNOWN; parentRoot: RootHex; parentBlockHash: RootHex}
159
159
  | {code: BlockErrorCode.NON_LINEAR_PAYLOAD_ROOTS; parentBlockHash: RootHex; expectedBlockHash: RootHex};
@@ -12,6 +12,8 @@ export enum ExecutionPayloadEnvelopeErrorCode {
12
12
  BUILDER_INDEX_MISMATCH = "EXECUTION_PAYLOAD_ENVELOPE_ERROR_BUILDER_INDEX_MISMATCH",
13
13
  BLOCK_HASH_MISMATCH = "EXECUTION_PAYLOAD_ENVELOPE_ERROR_BLOCK_HASH_MISMATCH",
14
14
  EXECUTION_REQUESTS_ROOT_MISMATCH = "EXECUTION_PAYLOAD_ENVELOPE_ERROR_EXECUTION_REQUESTS_ROOT_MISMATCH",
15
+ EXECUTION_REQUESTS_COUNT_EXCEEDED = "EXECUTION_PAYLOAD_ENVELOPE_ERROR_EXECUTION_REQUESTS_COUNT_EXCEEDED",
16
+ WITHDRAWALS_COUNT_EXCEEDED = "EXECUTION_PAYLOAD_ENVELOPE_ERROR_WITHDRAWALS_COUNT_EXCEEDED",
15
17
  INVALID_SIGNATURE = "EXECUTION_PAYLOAD_ENVELOPE_ERROR_INVALID_SIGNATURE",
16
18
  PAYLOAD_ENVELOPE_INPUT_MISSING = "EXECUTION_PAYLOAD_ENVELOPE_ERROR_PAYLOAD_ENVELOPE_INPUT_MISSING",
17
19
  }
@@ -42,6 +44,13 @@ export type ExecutionPayloadEnvelopeErrorType =
42
44
  envelopeRequestsRoot: RootHex;
43
45
  bidRequestsRoot: RootHex;
44
46
  }
47
+ | {
48
+ code: ExecutionPayloadEnvelopeErrorCode.EXECUTION_REQUESTS_COUNT_EXCEEDED;
49
+ name: string;
50
+ count: number;
51
+ limit: number;
52
+ }
53
+ | {code: ExecutionPayloadEnvelopeErrorCode.WITHDRAWALS_COUNT_EXCEEDED; count: number; limit: number}
45
54
  | {code: ExecutionPayloadEnvelopeErrorCode.INVALID_SIGNATURE}
46
55
  | {code: ExecutionPayloadEnvelopeErrorCode.PAYLOAD_ENVELOPE_INPUT_MISSING; blockRoot: RootHex};
47
56
 
@@ -528,18 +528,23 @@ export async function produceBlockBody<T extends BlockType>(
528
528
  }
529
529
  }
530
530
 
531
- const {blobKzgCommitments, executionRequests} = builderRes;
532
- if (blobKzgCommitments === undefined) {
533
- throw Error(`Invalid builder getHeader response for fork=${fork}, missing blobKzgCommitments`);
534
- }
531
+ if (ForkSeq[fork] >= ForkSeq.deneb) {
532
+ const {blobKzgCommitments} = builderRes;
533
+ if (blobKzgCommitments === undefined) {
534
+ throw Error(`Invalid builder getHeader response for fork=${fork}, missing blobKzgCommitments`);
535
+ }
535
536
 
536
- (blockBody as BlindedBeaconBlockBody<ForkName.fulu>).blobKzgCommitments = blobKzgCommitments;
537
- Object.assign(logMeta, {blobs: blobKzgCommitments.length});
537
+ (blockBody as deneb.BlindedBeaconBlockBody).blobKzgCommitments = blobKzgCommitments;
538
+ Object.assign(logMeta, {blobs: blobKzgCommitments.length});
539
+ }
538
540
 
539
- if (executionRequests === undefined) {
540
- throw Error(`Invalid builder getHeader response for fork=${fork}, missing executionRequests`);
541
+ if (ForkSeq[fork] >= ForkSeq.electra) {
542
+ const {executionRequests} = builderRes;
543
+ if (executionRequests === undefined) {
544
+ throw Error(`Invalid builder getHeader response for fork=${fork}, missing executionRequests`);
545
+ }
546
+ (blockBody as electra.BlindedBeaconBlockBody).executionRequests = executionRequests;
541
547
  }
542
- (blockBody as BlindedBeaconBlockBody<ForkName.fulu>).executionRequests = executionRequests;
543
548
  }
544
549
 
545
550
  // blockType === BlockType.Full
@@ -894,38 +899,45 @@ function preparePayloadAttributes(
894
899
  ): SSEPayloadAttributes["payloadAttributes"] {
895
900
  const timestamp = computeTimeAtSlot(chain.config, prepareSlot, prepareState.genesisTime);
896
901
  const prevRandao = prepareState.getRandaoMix(prepareState.epoch);
897
- if (!isStatePostCapella(prepareState)) {
898
- throw new Error("Expected Capella state for withdrawals");
899
- }
902
+ const payloadAttributes = {
903
+ timestamp,
904
+ prevRandao,
905
+ suggestedFeeRecipient: feeRecipient,
906
+ };
900
907
 
901
- let withdrawals: capella.Withdrawal[];
902
- if (isStatePostGloas(prepareState)) {
903
- const isExtendingPayload = byteArrayEquals(parentBlockHash, prepareState.latestExecutionPayloadBid.blockHash);
904
- if (isExtendingPayload) {
905
- // applyParentExecutionPayload sets latestBlockHash = parentBid.blockHash, so a mismatch
906
- // here means the caller did not apply parent payload to prepareState
907
- if (!byteArrayEquals(prepareState.latestBlockHash, prepareState.latestExecutionPayloadBid.blockHash)) {
908
- throw new Error("Expected state with parent execution payload applied for withdrawals");
908
+ if (ForkSeq[fork] >= ForkSeq.capella) {
909
+ if (!isStatePostCapella(prepareState)) {
910
+ throw new Error("Expected Capella state for withdrawals");
911
+ }
912
+
913
+ if (isStatePostGloas(prepareState)) {
914
+ const isExtendingPayload = byteArrayEquals(parentBlockHash, prepareState.latestExecutionPayloadBid.blockHash);
915
+ if (isExtendingPayload) {
916
+ // applyParentExecutionPayload sets latestBlockHash = parentBid.blockHash, so a mismatch
917
+ // here means the caller did not apply parent payload to prepareState
918
+ if (!byteArrayEquals(prepareState.latestBlockHash, prepareState.latestExecutionPayloadBid.blockHash)) {
919
+ throw new Error("Expected state with parent execution payload applied for withdrawals");
920
+ }
921
+ (payloadAttributes as capella.SSEPayloadAttributes["payloadAttributes"]).withdrawals =
922
+ prepareState.getExpectedWithdrawals().expectedWithdrawals;
923
+ } else {
924
+ // When the parent block is empty, state.payloadExpectedWithdrawals holds a batch
925
+ // already deducted from CL balances but never credited on the EL (the envelope
926
+ // was not delivered). The next payload must carry those same withdrawals to
927
+ // restore CL/EL consistency, otherwise validators permanently lose that balance.
928
+ (payloadAttributes as capella.SSEPayloadAttributes["payloadAttributes"]).withdrawals =
929
+ prepareState.payloadExpectedWithdrawals;
909
930
  }
910
- withdrawals = prepareState.getExpectedWithdrawals().expectedWithdrawals;
911
931
  } else {
912
- // When the parent block is empty, state.payloadExpectedWithdrawals holds a batch
913
- // already deducted from CL balances but never credited on the EL (the envelope
914
- // was not delivered). The next payload must carry those same withdrawals to
915
- // restore CL/EL consistency, otherwise validators permanently lose that balance.
916
- withdrawals = prepareState.payloadExpectedWithdrawals;
932
+ // withdrawals logic is now fork aware as it changes on electra fork post capella
933
+ (payloadAttributes as capella.SSEPayloadAttributes["payloadAttributes"]).withdrawals =
934
+ prepareState.getExpectedWithdrawals().expectedWithdrawals;
917
935
  }
918
- } else {
919
- withdrawals = prepareState.getExpectedWithdrawals().expectedWithdrawals;
920
936
  }
921
937
 
922
- const payloadAttributes: deneb.SSEPayloadAttributes["payloadAttributes"] = {
923
- timestamp,
924
- prevRandao,
925
- suggestedFeeRecipient: feeRecipient,
926
- withdrawals,
927
- parentBeaconBlockRoot: parentBlockRoot,
928
- };
938
+ if (ForkSeq[fork] >= ForkSeq.deneb) {
939
+ (payloadAttributes as deneb.SSEPayloadAttributes["payloadAttributes"]).parentBeaconBlockRoot = parentBlockRoot;
940
+ }
929
941
 
930
942
  if (ForkSeq[fork] >= ForkSeq.gloas) {
931
943
  if (!isStatePostGloas(prepareState)) {
@@ -1038,28 +1050,37 @@ export async function produceCommonBlockBody<T extends BlockType>(
1038
1050
  step: BlockProductionStep.attestations,
1039
1051
  });
1040
1052
 
1041
- const endSyncAggregate = stepsMetrics?.startTimer();
1042
- const parentBlockRoot = fromHex(parentBlock.blockRoot);
1043
- const previousSlot = slot - 1;
1044
- const syncAggregate = this.syncContributionAndProofPool.getAggregate(previousSlot, parentBlockRoot);
1045
- this.metrics?.production.producedSyncAggregateParticipants.observe(
1046
- syncAggregate.syncCommitteeBits.getTrueBitIndexes().length
1047
- );
1048
- endSyncAggregate?.({
1049
- step: BlockProductionStep.syncAggregate,
1050
- });
1051
-
1052
- // Live proposal production is supported from Fulu onward.
1053
- return {
1053
+ const blockBody: Omit<CommonBlockBody, "blsToExecutionChanges" | "syncAggregate"> = {
1054
1054
  randaoReveal,
1055
1055
  graffiti,
1056
+ // Eth1 data voting is no longer required since electra
1056
1057
  eth1Data: currentState.eth1Data,
1057
1058
  proposerSlashings: this.opts.disableProposerSlashings === true ? [] : proposerSlashings,
1058
1059
  attesterSlashings,
1059
1060
  attestations,
1061
+ // Since electra, deposits are processed by the execution layer,
1062
+ // we no longer support handling deposits from earlier forks.
1060
1063
  deposits: [],
1061
1064
  voluntaryExits,
1062
- blsToExecutionChanges,
1063
- syncAggregate,
1064
1065
  };
1066
+
1067
+ if (ForkSeq[fork] >= ForkSeq.capella) {
1068
+ (blockBody as CommonBlockBody).blsToExecutionChanges = blsToExecutionChanges;
1069
+ }
1070
+
1071
+ const endSyncAggregate = stepsMetrics?.startTimer();
1072
+ if (ForkSeq[fork] >= ForkSeq.altair) {
1073
+ const parentBlockRoot = fromHex(parentBlock.blockRoot);
1074
+ const previousSlot = slot - 1;
1075
+ const syncAggregate = this.syncContributionAndProofPool.getAggregate(previousSlot, parentBlockRoot);
1076
+ this.metrics?.production.producedSyncAggregateParticipants.observe(
1077
+ syncAggregate.syncCommitteeBits.getTrueBitIndexes().length
1078
+ );
1079
+ (blockBody as CommonBlockBody).syncAggregate = syncAggregate;
1080
+ }
1081
+ endSyncAggregate?.({
1082
+ step: BlockProductionStep.syncAggregate,
1083
+ });
1084
+
1085
+ return blockBody as CommonBlockBody;
1065
1086
  }
@@ -1,6 +1,22 @@
1
1
  import {ChainForkConfig} from "@lodestar/config";
2
2
  import {ExecutionStatus} from "@lodestar/fork-choice";
3
- import {ForkName, MIN_SEED_LOOKAHEAD, isForkPostBellatrix, isForkPostDeneb, isForkPostGloas} from "@lodestar/params";
3
+ import {
4
+ ForkName,
5
+ MAX_ATTESTATIONS_ELECTRA,
6
+ MAX_ATTESTER_SLASHINGS_ELECTRA,
7
+ MAX_BLS_TO_EXECUTION_CHANGES,
8
+ MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD,
9
+ MAX_BUILDER_EXIT_REQUESTS_PER_PAYLOAD,
10
+ MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD,
11
+ MAX_PAYLOAD_ATTESTATIONS,
12
+ MAX_PROPOSER_SLASHINGS,
13
+ MAX_VOLUNTARY_EXITS,
14
+ MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD,
15
+ MIN_SEED_LOOKAHEAD,
16
+ isForkPostBellatrix,
17
+ isForkPostDeneb,
18
+ isForkPostGloas,
19
+ } from "@lodestar/params";
4
20
  import {
5
21
  computeEpochAtSlot,
6
22
  computeStartSlotAtEpoch,
@@ -157,8 +173,7 @@ export async function validateGossipBlock(
157
173
  }
158
174
 
159
175
  if (isForkPostGloas(fork)) {
160
- const body = (block as gloas.BeaconBlock).body;
161
- const bid = body.signedExecutionPayloadBid.message;
176
+ const bid = (block as gloas.BeaconBlock).body.signedExecutionPayloadBid.message;
162
177
 
163
178
  // [REJECT] The length of KZG commitments is less than or equal to the limitation defined in Consensus Layer
164
179
  // -- i.e. validate that len(bid.blob_kzg_commitments) <= max_blobs_per_block
@@ -181,12 +196,54 @@ export async function validateGossipBlock(
181
196
  });
182
197
  }
183
198
 
184
- // [REJECT] The block contains no deposits.
185
- if (body.deposits.length !== 0) {
186
- throw new BlockGossipError(GossipAction.REJECT, {
187
- code: BlockErrorCode.NON_ZERO_DEPOSITS,
188
- count: body.deposits.length,
189
- });
199
+ // [REJECT] The counts of `block.body.parent_execution_requests` are within
200
+ // their respective limits -- i.e. validate that
201
+ // `len(block.body.parent_execution_requests.withdrawals) <= MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD`,
202
+ // `len(block.body.parent_execution_requests.consolidations) <= MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD`,
203
+ // `len(block.body.parent_execution_requests.builder_deposits) <= MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD`,
204
+ // and
205
+ // `len(block.body.parent_execution_requests.builder_exits) <= MAX_BUILDER_EXIT_REQUESTS_PER_PAYLOAD`.
206
+ // [REJECT] The counts of the block body operations are within their respective
207
+ // limits -- i.e. validate that
208
+ // `len(block.body.proposer_slashings) <= MAX_PROPOSER_SLASHINGS`,
209
+ // `len(block.body.attester_slashings) <= MAX_ATTESTER_SLASHINGS_ELECTRA`,
210
+ // `len(block.body.attestations) <= MAX_ATTESTATIONS_ELECTRA`,
211
+ // `len(block.body.deposits) == 0`,
212
+ // `len(block.body.voluntary_exits) <= MAX_VOLUNTARY_EXITS`,
213
+ // `len(block.body.bls_to_execution_changes) <= MAX_BLS_TO_EXECUTION_CHANGES`,
214
+ // and `len(block.body.payload_attestations) <= MAX_PAYLOAD_ATTESTATIONS`.
215
+ const body = (block as gloas.BeaconBlock).body;
216
+ const requests = body.parentExecutionRequests;
217
+ const countLimits: [string, number, number][] = [
218
+ ["parentExecutionRequests.withdrawals", requests.withdrawals.length, MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD],
219
+ [
220
+ "parentExecutionRequests.consolidations",
221
+ requests.consolidations.length,
222
+ MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD,
223
+ ],
224
+ [
225
+ "parentExecutionRequests.builderDeposits",
226
+ requests.builderDeposits.length,
227
+ MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD,
228
+ ],
229
+ ["parentExecutionRequests.builderExits", requests.builderExits.length, MAX_BUILDER_EXIT_REQUESTS_PER_PAYLOAD],
230
+ ["proposerSlashings", body.proposerSlashings.length, MAX_PROPOSER_SLASHINGS],
231
+ ["attesterSlashings", body.attesterSlashings.length, MAX_ATTESTER_SLASHINGS_ELECTRA],
232
+ ["attestations", body.attestations.length, MAX_ATTESTATIONS_ELECTRA],
233
+ ["deposits", body.deposits.length, 0],
234
+ ["voluntaryExits", body.voluntaryExits.length, MAX_VOLUNTARY_EXITS],
235
+ ["blsToExecutionChanges", body.blsToExecutionChanges.length, MAX_BLS_TO_EXECUTION_CHANGES],
236
+ ["payloadAttestations", body.payloadAttestations.length, MAX_PAYLOAD_ATTESTATIONS],
237
+ ];
238
+ for (const [name, count, limit] of countLimits) {
239
+ if (count > limit) {
240
+ throw new BlockGossipError(GossipAction.REJECT, {
241
+ code: BlockErrorCode.TOO_MANY_BLOCK_OPERATIONS,
242
+ name,
243
+ count,
244
+ limit,
245
+ });
246
+ }
190
247
  }
191
248
 
192
249
  // TODO GLOAS: [REJECT] The block's execution payload parent (defined by bid.parent_block_hash) passes all validation
@@ -1,4 +1,11 @@
1
1
  import {PayloadStatus} from "@lodestar/fork-choice";
2
+ import {
3
+ MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD,
4
+ MAX_BUILDER_EXIT_REQUESTS_PER_PAYLOAD,
5
+ MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD,
6
+ MAX_WITHDRAWALS_PER_PAYLOAD,
7
+ MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD,
8
+ } from "@lodestar/params";
2
9
  import {
3
10
  computeStartSlotAtEpoch,
4
11
  getExecutionPayloadEnvelopeSignatureSet,
@@ -115,6 +122,36 @@ async function validateExecutionPayloadEnvelope(
115
122
  });
116
123
  }
117
124
 
125
+ // [REJECT] The counts of `execution_requests` are within their respective limits.
126
+ // New in Gloas:EIP7688 — progressive lists are unbounded at the type level, so bounds
127
+ // are enforced here in gossip validation.
128
+ const {executionRequests} = envelope;
129
+ const requestCountLimits: [string, number, number][] = [
130
+ ["withdrawals", executionRequests.withdrawals.length, MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD],
131
+ ["consolidations", executionRequests.consolidations.length, MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD],
132
+ ["builderDeposits", executionRequests.builderDeposits.length, MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD],
133
+ ["builderExits", executionRequests.builderExits.length, MAX_BUILDER_EXIT_REQUESTS_PER_PAYLOAD],
134
+ ];
135
+ for (const [name, count, limit] of requestCountLimits) {
136
+ if (count > limit) {
137
+ throw new ExecutionPayloadEnvelopeError(GossipAction.REJECT, {
138
+ code: ExecutionPayloadEnvelopeErrorCode.EXECUTION_REQUESTS_COUNT_EXCEEDED,
139
+ name,
140
+ count,
141
+ limit,
142
+ });
143
+ }
144
+ }
145
+
146
+ // [REJECT] The number of withdrawals is within the limit.
147
+ if (payload.withdrawals.length > MAX_WITHDRAWALS_PER_PAYLOAD) {
148
+ throw new ExecutionPayloadEnvelopeError(GossipAction.REJECT, {
149
+ code: ExecutionPayloadEnvelopeErrorCode.WITHDRAWALS_COUNT_EXCEEDED,
150
+ count: payload.withdrawals.length,
151
+ limit: MAX_WITHDRAWALS_PER_PAYLOAD,
152
+ });
153
+ }
154
+
118
155
  // Get the block state to verify the builder's signature.
119
156
  const blockState = await chain.regen
120
157
  .getState(block.stateRoot, RegenCaller.validateGossipPayloadEnvelope)