@lodestar/beacon-node 1.48.0-dev.b0ce010779 → 1.48.0-dev.b0ddac0ac2

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 (76) hide show
  1. package/lib/api/impl/validator/index.d.ts.map +1 -1
  2. package/lib/api/impl/validator/index.js +39 -39
  3. package/lib/api/impl/validator/index.js.map +1 -1
  4. package/lib/chain/archiveStore/historicalState/metrics.d.ts.map +1 -1
  5. package/lib/chain/archiveStore/historicalState/metrics.js +12 -1
  6. package/lib/chain/archiveStore/historicalState/metrics.js.map +1 -1
  7. package/lib/chain/blocks/importBlock.d.ts.map +1 -1
  8. package/lib/chain/blocks/importBlock.js +0 -13
  9. package/lib/chain/blocks/importBlock.js.map +1 -1
  10. package/lib/chain/blocks/utils/chainSegment.d.ts +1 -1
  11. package/lib/chain/blocks/utils/chainSegment.d.ts.map +1 -1
  12. package/lib/chain/blocks/utils/chainSegment.js +10 -1
  13. package/lib/chain/blocks/utils/chainSegment.js.map +1 -1
  14. package/lib/chain/chain.d.ts +5 -0
  15. package/lib/chain/chain.d.ts.map +1 -1
  16. package/lib/chain/chain.js +67 -38
  17. package/lib/chain/chain.js.map +1 -1
  18. package/lib/chain/emitter.d.ts +1 -1
  19. package/lib/chain/emitter.js +1 -1
  20. package/lib/chain/errors/blockError.d.ts +3 -5
  21. package/lib/chain/errors/blockError.d.ts.map +1 -1
  22. package/lib/chain/errors/blockError.js +2 -2
  23. package/lib/chain/errors/blockError.js.map +1 -1
  24. package/lib/chain/errors/executionPayloadEnvelope.d.ts +0 -11
  25. package/lib/chain/errors/executionPayloadEnvelope.d.ts.map +1 -1
  26. package/lib/chain/errors/executionPayloadEnvelope.js +0 -2
  27. package/lib/chain/errors/executionPayloadEnvelope.js.map +1 -1
  28. package/lib/chain/prepareNextSlot.js +13 -13
  29. package/lib/chain/prepareNextSlot.js.map +1 -1
  30. package/lib/chain/produceBlock/produceBlockBody.d.ts.map +1 -1
  31. package/lib/chain/produceBlock/produceBlockBody.js +46 -64
  32. package/lib/chain/produceBlock/produceBlockBody.js.map +1 -1
  33. package/lib/chain/validation/block.d.ts.map +1 -1
  34. package/lib/chain/validation/block.js +9 -50
  35. package/lib/chain/validation/block.js.map +1 -1
  36. package/lib/chain/validation/executionPayloadEnvelope.d.ts.map +1 -1
  37. package/lib/chain/validation/executionPayloadEnvelope.js +0 -29
  38. package/lib/chain/validation/executionPayloadEnvelope.js.map +1 -1
  39. package/lib/chain/validatorMonitor.d.ts +7 -1
  40. package/lib/chain/validatorMonitor.d.ts.map +1 -1
  41. package/lib/chain/validatorMonitor.js +22 -5
  42. package/lib/chain/validatorMonitor.js.map +1 -1
  43. package/lib/execution/engine/mock.d.ts.map +1 -1
  44. package/lib/execution/engine/mock.js +4 -1
  45. package/lib/execution/engine/mock.js.map +1 -1
  46. package/lib/network/gossip/encoding.d.ts.map +1 -1
  47. package/lib/network/gossip/encoding.js +0 -6
  48. package/lib/network/gossip/encoding.js.map +1 -1
  49. package/lib/network/gossip/topic.d.ts +2 -1
  50. package/lib/network/gossip/topic.d.ts.map +1 -1
  51. package/lib/network/gossip/topic.js +4 -20
  52. package/lib/network/gossip/topic.js.map +1 -1
  53. package/lib/network/reqresp/protocols.d.ts.map +1 -1
  54. package/lib/network/reqresp/protocols.js +6 -22
  55. package/lib/network/reqresp/protocols.js.map +1 -1
  56. package/lib/network/reqresp/rateLimit.js +2 -1
  57. package/lib/network/reqresp/rateLimit.js.map +1 -1
  58. package/package.json +15 -15
  59. package/src/api/impl/validator/index.ts +44 -43
  60. package/src/chain/archiveStore/historicalState/metrics.ts +14 -1
  61. package/src/chain/blocks/importBlock.ts +0 -13
  62. package/src/chain/blocks/utils/chainSegment.ts +10 -1
  63. package/src/chain/chain.ts +74 -46
  64. package/src/chain/emitter.ts +1 -1
  65. package/src/chain/errors/blockError.ts +3 -3
  66. package/src/chain/errors/executionPayloadEnvelope.ts +0 -9
  67. package/src/chain/prepareNextSlot.ts +16 -16
  68. package/src/chain/produceBlock/produceBlockBody.ts +50 -71
  69. package/src/chain/validation/block.ts +9 -66
  70. package/src/chain/validation/executionPayloadEnvelope.ts +0 -37
  71. package/src/chain/validatorMonitor.ts +24 -7
  72. package/src/execution/engine/mock.ts +4 -1
  73. package/src/network/gossip/encoding.ts +0 -6
  74. package/src/network/gossip/topic.ts +3 -25
  75. package/src/network/reqresp/protocols.ts +7 -24
  76. package/src/network/reqresp/rateLimit.ts +2 -1
@@ -528,23 +528,18 @@ export async function produceBlockBody<T extends BlockType>(
528
528
  }
529
529
  }
530
530
 
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
- }
536
-
537
- (blockBody as deneb.BlindedBeaconBlockBody).blobKzgCommitments = blobKzgCommitments;
538
- Object.assign(logMeta, {blobs: blobKzgCommitments.length});
531
+ const {blobKzgCommitments, executionRequests} = builderRes;
532
+ if (blobKzgCommitments === undefined) {
533
+ throw Error(`Invalid builder getHeader response for fork=${fork}, missing blobKzgCommitments`);
539
534
  }
540
535
 
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;
536
+ (blockBody as BlindedBeaconBlockBody<ForkName.fulu>).blobKzgCommitments = blobKzgCommitments;
537
+ Object.assign(logMeta, {blobs: blobKzgCommitments.length});
538
+
539
+ if (executionRequests === undefined) {
540
+ throw Error(`Invalid builder getHeader response for fork=${fork}, missing executionRequests`);
547
541
  }
542
+ (blockBody as BlindedBeaconBlockBody<ForkName.fulu>).executionRequests = executionRequests;
548
543
  }
549
544
 
550
545
  // blockType === BlockType.Full
@@ -899,45 +894,38 @@ function preparePayloadAttributes(
899
894
  ): SSEPayloadAttributes["payloadAttributes"] {
900
895
  const timestamp = computeTimeAtSlot(chain.config, prepareSlot, prepareState.genesisTime);
901
896
  const prevRandao = prepareState.getRandaoMix(prepareState.epoch);
902
- const payloadAttributes = {
903
- timestamp,
904
- prevRandao,
905
- suggestedFeeRecipient: feeRecipient,
906
- };
907
-
908
- if (ForkSeq[fork] >= ForkSeq.capella) {
909
- if (!isStatePostCapella(prepareState)) {
910
- throw new Error("Expected Capella state for withdrawals");
911
- }
897
+ if (!isStatePostCapella(prepareState)) {
898
+ throw new Error("Expected Capella state for withdrawals");
899
+ }
912
900
 
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;
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");
930
909
  }
910
+ withdrawals = prepareState.getExpectedWithdrawals().expectedWithdrawals;
931
911
  } else {
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;
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;
935
917
  }
918
+ } else {
919
+ withdrawals = prepareState.getExpectedWithdrawals().expectedWithdrawals;
936
920
  }
937
921
 
938
- if (ForkSeq[fork] >= ForkSeq.deneb) {
939
- (payloadAttributes as deneb.SSEPayloadAttributes["payloadAttributes"]).parentBeaconBlockRoot = parentBlockRoot;
940
- }
922
+ const payloadAttributes: deneb.SSEPayloadAttributes["payloadAttributes"] = {
923
+ timestamp,
924
+ prevRandao,
925
+ suggestedFeeRecipient: feeRecipient,
926
+ withdrawals,
927
+ parentBeaconBlockRoot: parentBlockRoot,
928
+ };
941
929
 
942
930
  if (ForkSeq[fork] >= ForkSeq.gloas) {
943
931
  if (!isStatePostGloas(prepareState)) {
@@ -1050,37 +1038,28 @@ export async function produceCommonBlockBody<T extends BlockType>(
1050
1038
  step: BlockProductionStep.attestations,
1051
1039
  });
1052
1040
 
1053
- const blockBody: Omit<CommonBlockBody, "blsToExecutionChanges" | "syncAggregate"> = {
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 {
1054
1054
  randaoReveal,
1055
1055
  graffiti,
1056
- // Eth1 data voting is no longer required since electra
1057
1056
  eth1Data: currentState.eth1Data,
1058
1057
  proposerSlashings: this.opts.disableProposerSlashings === true ? [] : proposerSlashings,
1059
1058
  attesterSlashings,
1060
1059
  attestations,
1061
- // Since electra, deposits are processed by the execution layer,
1062
- // we no longer support handling deposits from earlier forks.
1063
1060
  deposits: [],
1064
1061
  voluntaryExits,
1062
+ blsToExecutionChanges,
1063
+ syncAggregate,
1065
1064
  };
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;
1086
1065
  }
@@ -1,22 +1,6 @@
1
1
  import {ChainForkConfig} from "@lodestar/config";
2
2
  import {ExecutionStatus} from "@lodestar/fork-choice";
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";
3
+ import {ForkName, MIN_SEED_LOOKAHEAD, isForkPostBellatrix, isForkPostDeneb, isForkPostGloas} from "@lodestar/params";
20
4
  import {
21
5
  computeEpochAtSlot,
22
6
  computeStartSlotAtEpoch,
@@ -173,7 +157,8 @@ export async function validateGossipBlock(
173
157
  }
174
158
 
175
159
  if (isForkPostGloas(fork)) {
176
- const bid = (block as gloas.BeaconBlock).body.signedExecutionPayloadBid.message;
160
+ const body = (block as gloas.BeaconBlock).body;
161
+ const bid = body.signedExecutionPayloadBid.message;
177
162
 
178
163
  // [REJECT] The length of KZG commitments is less than or equal to the limitation defined in Consensus Layer
179
164
  // -- i.e. validate that len(bid.blob_kzg_commitments) <= max_blobs_per_block
@@ -196,54 +181,12 @@ export async function validateGossipBlock(
196
181
  });
197
182
  }
198
183
 
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
- }
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
+ });
247
190
  }
248
191
 
249
192
  // TODO GLOAS: [REJECT] The block's execution payload parent (defined by bid.parent_block_hash) passes all validation
@@ -1,11 +1,4 @@
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";
9
2
  import {
10
3
  computeStartSlotAtEpoch,
11
4
  getExecutionPayloadEnvelopeSignatureSet,
@@ -122,36 +115,6 @@ async function validateExecutionPayloadEnvelope(
122
115
  });
123
116
  }
124
117
 
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
-
155
118
  // Get the block state to verify the builder's signature.
156
119
  const blockState = await chain.regen
157
120
  .getState(block.stateRoot, RegenCaller.validateGossipPayloadEnvelope)
@@ -1,5 +1,5 @@
1
1
  import {ChainForkConfig} from "@lodestar/config";
2
- import {MIN_ATTESTATION_INCLUSION_DELAY, SLOTS_PER_EPOCH} from "@lodestar/params";
2
+ import {MIN_ATTESTATION_INCLUSION_DELAY, SLOTS_PER_EPOCH, SLOTS_PER_HISTORICAL_ROOT} from "@lodestar/params";
3
3
  import {
4
4
  IBeaconStateView,
5
5
  ParticipationFlags,
@@ -1117,7 +1117,11 @@ function isCanonical(rootCache: RootHexCache, block: AttestationBlockInclusion):
1117
1117
 
1118
1118
  /** Returns true if root at slot is the same at slot - 1 == there was no new block at slot */
1119
1119
  function isMissedSlot(rootCache: RootHexCache, slot: Slot): boolean {
1120
- return slot > 0 && rootCache.getBlockRootAtSlot(slot) === rootCache.getBlockRootAtSlot(slot - 1);
1120
+ if (slot <= 0) {
1121
+ return false;
1122
+ }
1123
+ const root = rootCache.getBlockRootAtSlot(slot);
1124
+ return root !== null && root === rootCache.getBlockRootAtSlot(slot - 1);
1121
1125
  }
1122
1126
 
1123
1127
  function renderBlockProposalSummary(
@@ -1131,7 +1135,11 @@ function renderBlockProposalSummary(
1131
1135
  return "not_submitted";
1132
1136
  }
1133
1137
 
1134
- if (rootCache.getBlockRootAtSlot(proposalSlot) === proposal.blockRoot) {
1138
+ const canonicalRoot = rootCache.getBlockRootAtSlot(proposalSlot);
1139
+ if (canonicalRoot === null) {
1140
+ return "unknown";
1141
+ }
1142
+ if (canonicalRoot === proposal.blockRoot) {
1135
1143
  // Canonical state includes our block
1136
1144
  return "canonical";
1137
1145
  }
@@ -1157,14 +1165,23 @@ function renderBlockProposalSummary(
1157
1165
  * In normal network conditions the same root is read multiple times, specially the target.
1158
1166
  */
1159
1167
  export class RootHexCache {
1160
- private readonly blockRootSlotCache = new Map<Slot, RootHex>();
1168
+ private readonly blockRootSlotCache = new Map<Slot, RootHex | null>();
1161
1169
 
1162
1170
  constructor(private readonly state: IBeaconStateView) {}
1163
1171
 
1164
- getBlockRootAtSlot(slot: Slot): RootHex {
1172
+ /**
1173
+ * Block root at `slot`, null if the state does not cover it: slots at or after the state's own slot (an inclusion
1174
+ * in a block the head state does not know yet, e.g. an orphaned block, or the slot after the last attestation of
1175
+ * the epoch) or slots older than `SLOTS_PER_HISTORICAL_ROOT`. Callers treat null as unknown instead of throwing
1176
+ * and aborting the summaries of all remaining validators.
1177
+ */
1178
+ getBlockRootAtSlot(slot: Slot): RootHex | null {
1165
1179
  let root = this.blockRootSlotCache.get(slot);
1166
- if (!root) {
1167
- root = toRootHex(this.state.getBlockRootAtSlot(slot));
1180
+ if (root === undefined) {
1181
+ root =
1182
+ slot < this.state.slot && slot >= this.state.slot - SLOTS_PER_HISTORICAL_ROOT
1183
+ ? toRootHex(this.state.getBlockRootAtSlot(slot))
1184
+ : null;
1168
1185
  this.blockRootSlotCache.set(slot, root);
1169
1186
  }
1170
1187
  return root;
@@ -296,7 +296,10 @@ export class ExecutionEngineMockBackend implements JsonRpcBackend {
296
296
 
297
297
  // 5. Client software MUST update its forkchoice state if payloads referenced by forkchoiceState.headBlockHash and
298
298
  // forkchoiceState.finalizedBlockHash are VALID.
299
- if (!this.validBlocks.has(finalizedBlockHash)) {
299
+ //
300
+ // A zero finalizedBlockHash means "no finalized execution block" (e.g. the finalized checkpoint has no execution
301
+ // payload, pre-merge/pre-TTD). Real EL clients accept this, so the mock must not treat it as an unknown block.
302
+ if (finalizedBlockHash !== ZERO_HASH_HEX && !this.validBlocks.has(finalizedBlockHash)) {
300
303
  throw Error(`Unknown finalizedBlockHash ${finalizedBlockHash}`);
301
304
  }
302
305
  this.headBlockHash = headBlockHash;
@@ -123,9 +123,6 @@ export class DataTransformSnappy implements DataTransform {
123
123
  if (uncompressedDataLength > maxSize) {
124
124
  throw Error(`ssz_snappy decoded data length ${uncompressedDataLength} > ${maxSize}`);
125
125
  }
126
- if (uncompressedDataLength > sszType.maxSize) {
127
- throw Error(`ssz_snappy decoded data length ${uncompressedDataLength} > ${sszType.maxSize}`);
128
- }
129
126
 
130
127
  // Only after sanity length checks, we can decompress the data
131
128
  // Using Buffer.alloc() instead of Buffer.allocUnsafe() to mitigate high GC pressure observed in some environments
@@ -146,9 +143,6 @@ export class DataTransformSnappy implements DataTransform {
146
143
  if (data.length > maxSize) {
147
144
  throw Error(`ssz_snappy encoded data length ${data.length} > ${maxSize}`);
148
145
  }
149
- if (data.length > sszType.maxSize) {
150
- throw Error(`ssz_snappy encoded data length ${data.length} > ${sszType.maxSize}`);
151
- }
152
146
 
153
147
  // Using Buffer.alloc() instead of Buffer.allocUnsafe() to mitigate high GC pressure observed in some environments
154
148
  const compressedData = Buffer.alloc(snappyWasm.max_compress_len(data.length));
@@ -4,17 +4,11 @@ import {
4
4
  ATTESTATION_SUBNET_COUNT,
5
5
  ForkName,
6
6
  ForkSeq,
7
- MAX_ATTESTER_SLASHING_SIZE,
8
- MAX_DATA_COLUMN_SIDECAR_SIZE,
9
- MAX_SIGNED_AGGREGATE_AND_PROOF_SIZE,
10
- MAX_SIGNED_EXECUTION_PAYLOAD_BID_SIZE,
11
- MAX_SIGNED_EXECUTION_PAYLOAD_BID_SIZE_HEZE,
12
7
  SYNC_COMMITTEE_SUBNET_COUNT,
13
8
  isForkPostAltair,
14
9
  isForkPostElectra,
15
10
  isForkPostFulu,
16
11
  isForkPostGloas,
17
- isForkPostHeze,
18
12
  } from "@lodestar/params";
19
13
  import {Attestation, SingleAttestation, ssz, sszTypesFor} from "@lodestar/types";
20
14
  import {GossipAction, GossipActionError, GossipErrorCode} from "../../chain/errors/gossipValidation.js";
@@ -139,27 +133,11 @@ export function getGossipSSZType(topic: GossipTopic) {
139
133
  }
140
134
 
141
135
  /**
142
- * Return the maximum uncompressed SSZ byte length accepted for a gossip object.
136
+ * Return the maximum uncompressed SSZ byte length accepted for a gossip object, the SSZ type max size
137
+ * or MAX_PAYLOAD_SIZE, whichever is smaller.
143
138
  */
144
139
  export function getGossipSSZMaxSize(topic: GossipTopic, maxPayloadSize: number, sszType?: CompositeTypeAny): number {
145
- const {fork} = topic.boundary;
146
- // Gloas progressive containers have broad theoretical SSZ max sizes; use the preset p2p bounds instead.
147
- switch (topic.type) {
148
- case GossipType.beacon_block:
149
- return maxPayloadSize;
150
- case GossipType.beacon_aggregate_and_proof:
151
- return isForkPostGloas(fork) ? MAX_SIGNED_AGGREGATE_AND_PROOF_SIZE : (sszType ?? getGossipSSZType(topic)).maxSize;
152
- case GossipType.attester_slashing:
153
- return isForkPostGloas(fork) ? MAX_ATTESTER_SLASHING_SIZE : (sszType ?? getGossipSSZType(topic)).maxSize;
154
- case GossipType.data_column_sidecar:
155
- return isForkPostGloas(fork) ? MAX_DATA_COLUMN_SIDECAR_SIZE : (sszType ?? getGossipSSZType(topic)).maxSize;
156
- case GossipType.execution_payload:
157
- return maxPayloadSize;
158
- case GossipType.execution_payload_bid:
159
- return isForkPostHeze(fork) ? MAX_SIGNED_EXECUTION_PAYLOAD_BID_SIZE_HEZE : MAX_SIGNED_EXECUTION_PAYLOAD_BID_SIZE;
160
- default:
161
- return (sszType ?? getGossipSSZType(topic)).maxSize;
162
- }
140
+ return Math.min((sszType ?? getGossipSSZType(topic)).maxSize, maxPayloadSize);
163
141
  }
164
142
 
165
143
  /**
@@ -1,5 +1,5 @@
1
1
  import {BeaconConfig} from "@lodestar/config";
2
- import {ForkName, MAX_DATA_COLUMN_SIDECAR_SIZE, isForkPostGloas} from "@lodestar/params";
2
+ import {ForkName} from "@lodestar/params";
3
3
  import {ContextBytesFactory, ContextBytesType, Encoding, TypeSizes} from "@lodestar/reqresp";
4
4
  import {rateLimitQuotas} from "./rateLimit.js";
5
5
  import {ProtocolNoHandler, ReqRespMethod, Version, requestSszTypeByMethod, responseSszTypeByMethod} from "./types.js";
@@ -151,35 +151,18 @@ function toProtocol(protocol: ProtocolSummary) {
151
151
  encoding: Encoding.SSZ_SNAPPY,
152
152
  contextBytes: toContextBytes(protocol.contextBytesType, config),
153
153
  inboundRateLimits: rateLimitQuotas(fork, config)[protocol.method],
154
- requestSizes: requestType === null ? null : clampTypeSizes(requestType, protocol.method, fork, config),
155
- responseSizes: (fork) =>
156
- clampTypeSizes(responseSszTypeByMethod[protocol.method](fork, protocol.version), protocol.method, fork, config),
154
+ requestSizes: requestType === null ? null : clampTypeSizes(requestType, config),
155
+ responseSizes: (fork) => clampTypeSizes(responseSszTypeByMethod[protocol.method](fork, protocol.version), config),
157
156
  };
158
157
  };
159
158
  }
160
159
 
161
160
  /**
162
- * Bound the sizes accepted from the ssz-snappy length-prefix. Gloas progressive containers have broad
163
- * theoretical SSZ max sizes so the preset p2p bounds must be used instead.
164
- *
165
- * The length-prefix must be within the size bounds derived from the payload SSZ type or `MAX_PAYLOAD_SIZE`,
166
- * whichever is smaller, see
167
- * https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.12/specs/phase0/p2p-interface.md#encoding-strategies.
168
- * Type-specific SSZ bounds supersede the bounds derived from the SSZ type, see
169
- * https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.12/specs/gloas/p2p-interface.md#type-specific-ssz-bounds.
161
+ * Length-prefix must be within the SSZ type bounds or MAX_PAYLOAD_SIZE, whichever is smaller
162
+ * https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.12/specs/phase0/p2p-interface.md#encoding-strategies
170
163
  */
171
- function clampTypeSizes(type: TypeSizes, method: ReqRespMethod, fork: ForkName, config: BeaconConfig): TypeSizes {
172
- let typeSpecificBound = config.MAX_PAYLOAD_SIZE;
173
- if (isForkPostGloas(fork)) {
174
- switch (method) {
175
- case ReqRespMethod.DataColumnSidecarsByRange:
176
- case ReqRespMethod.DataColumnSidecarsByRoot:
177
- typeSpecificBound = MAX_DATA_COLUMN_SIDECAR_SIZE;
178
- break;
179
- }
180
- }
181
-
182
- return {minSize: type.minSize, maxSize: Math.min(type.maxSize, typeSpecificBound)};
164
+ function clampTypeSizes(type: TypeSizes, config: BeaconConfig): TypeSizes {
165
+ return {minSize: type.minSize, maxSize: Math.min(type.maxSize, config.MAX_PAYLOAD_SIZE)};
183
166
  }
184
167
 
185
168
  function toContextBytes(type: ContextBytesType, config: BeaconConfig): ContextBytesFactory {
@@ -126,7 +126,8 @@ function getRequestCountFn<T extends ReqRespMethod>(
126
126
  const type = requestSszTypeByMethod(fork, config)[method];
127
127
  return (reqData: Uint8Array) => {
128
128
  try {
129
- return (type && fn(type.deserialize(reqData))) ?? 1;
129
+ // A schema-valid request can cost 0 tokens; floor to 1 so it stays charged and bannable, not free.
130
+ return Math.max(1, (type && fn(type.deserialize(reqData))) ?? 1);
130
131
  } catch (_e) {
131
132
  return 1;
132
133
  }