@lodestar/beacon-node 1.43.0 → 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
@@ -14,6 +14,7 @@ import {
14
14
  isForkPostBellatrix,
15
15
  isForkPostDeneb,
16
16
  isForkPostElectra,
17
+ isForkPostFulu,
17
18
  isForkPostGloas,
18
19
  } from "@lodestar/params";
19
20
  import {
@@ -913,20 +914,38 @@ export function getValidatorApi(
913
914
  notWhileSyncing();
914
915
  await waitForSlot(slot);
915
916
 
916
- // TODO GLOAS: support producing blocks from builder bids
917
- const source = ProducedBlockSource.engine;
918
-
919
- // TODO GLOAS: needs to be updated after fork choice changes are merged
920
917
  const parentBlock = chain.getProposerHead(slot);
921
918
  const {blockRoot: parentBlockRootHex, slot: parentSlot} = parentBlock;
922
919
  const parentBlockRoot = fromHex(parentBlockRootHex);
923
920
  notOnOutOfRangeData(parentBlockRoot);
924
921
  metrics?.blockProductionSlotDelta.set(slot - parentSlot);
925
- metrics?.blockProductionRequests.inc({source});
926
922
 
927
923
  const graffitiBytes = toGraffitiBytes(
928
- graffiti ?? getDefaultGraffiti(getLodestarClientVersion(), chain.executionEngine.clientVersion, {})
924
+ graffiti ?? getDefaultGraffiti(getLodestarClientVersion(opts), chain.executionEngine.clientVersion, opts)
929
925
  );
926
+
927
+ // TODO GLOAS: respect builderSelection (MaxProfit, BuilderAlways, ExecutionAlways, etc.) to let
928
+ // the user control bid source preferences and value comparison. Also add external builder api
929
+ // support when it is implemented.
930
+ const isBuildingOnFull = chain.forkChoice.shouldBuildOnFull(parentBlock, slot);
931
+ const bidParentBlockHash = isBuildingOnFull ? parentBlock.executionPayloadBlockHash : parentBlock.parentBlockHash;
932
+ const builderBid = chain.executionPayloadBidPool.getBestBid(slot, bidParentBlockHash, parentBlockRootHex);
933
+
934
+ const logCtx = {
935
+ slot,
936
+ parentSlot,
937
+ parentBlockRoot: parentBlockRootHex,
938
+ parentBlockHash: parentBlock.executionPayloadBlockHash,
939
+ fork,
940
+ ...(builderBid !== null
941
+ ? {
942
+ bidValue: builderBid.message.value,
943
+ builderIndex: builderBid.message.builderIndex,
944
+ bidBlockHash: toRootHex(builderBid.message.blockHash),
945
+ }
946
+ : {}),
947
+ };
948
+
930
949
  const commonBlockBodyPromise = chain.produceCommonBlockBody({
931
950
  slot,
932
951
  parentBlock,
@@ -934,44 +953,76 @@ export function getValidatorApi(
934
953
  graffiti: graffitiBytes,
935
954
  });
936
955
 
937
- let timer: undefined | ((opts: {source: ProducedBlockSource}) => number);
938
- try {
939
- timer = metrics?.blockProductionTime.startTimer();
940
- const {block, executionPayloadValue, consensusBlockValue} = await chain.produceBlock({
941
- slot,
942
- parentBlock,
943
- randaoReveal,
944
- graffiti: graffitiBytes,
945
- feeRecipient,
946
- commonBlockBodyPromise,
947
- });
956
+ const baseAttrs = {
957
+ slot,
958
+ parentBlock,
959
+ randaoReveal,
960
+ graffiti: graffitiBytes,
961
+ feeRecipient,
962
+ commonBlockBodyPromise,
963
+ };
948
964
 
949
- metrics?.blockProductionSuccess.inc({source});
950
- metrics?.blockProductionNumAggregated.observe({source}, block.body.attestations.length);
951
- metrics?.blockProductionConsensusBlockValue.observe({source}, Number(formatWeiToEth(consensusBlockValue)));
952
- metrics?.blockProductionExecutionPayloadValue.observe({source}, Number(formatWeiToEth(executionPayloadValue)));
965
+ metrics?.blockProductionRequests.inc({source: ProducedBlockSource.engine});
966
+ if (builderBid !== null) {
967
+ metrics?.blockProductionRequests.inc({source: ProducedBlockSource.builder});
968
+ }
953
969
 
954
- const blockRoot = toRootHex(config.getForkTypes(slot).BeaconBlock.hashTreeRoot(block));
955
- logger.verbose("Produced block", {
956
- slot,
957
- executionPayloadValue,
958
- consensusBlockValue,
959
- root: blockRoot,
960
- });
961
- if (chain.opts.persistProducedBlocks) {
962
- void chain.persistBlock(block, "produced_engine_block");
970
+ const timed = <T>(source: ProducedBlockSource, fn: () => Promise<T>): Promise<T> => {
971
+ const t = metrics?.blockProductionTime.startTimer();
972
+ return fn().finally(() => t?.({source}));
973
+ };
974
+
975
+ // Always build local block. If builder bid available, also build with it in parallel and prefer it.
976
+ const [engineResult, bidResult] = await Promise.allSettled([
977
+ timed(ProducedBlockSource.engine, () => chain.produceBlock(baseAttrs)),
978
+ builderBid !== null
979
+ ? timed(ProducedBlockSource.builder, () => chain.produceBlock({...baseAttrs, builderBid}))
980
+ : Promise.reject(),
981
+ ]);
982
+
983
+ let bestResult: typeof engineResult | null = null;
984
+ let source: ProducedBlockSource = ProducedBlockSource.engine;
985
+ if (builderBid !== null && bidResult.status === "fulfilled") {
986
+ source = ProducedBlockSource.builder;
987
+ bestResult = bidResult;
988
+ logger.info("Selected builder bid block", logCtx);
989
+ } else if (engineResult.status === "fulfilled") {
990
+ source = ProducedBlockSource.engine;
991
+ bestResult = engineResult;
992
+ if (builderBid !== null) {
993
+ logger.warn("Builder bid block production failed, using local block", logCtx);
963
994
  }
995
+ }
964
996
 
965
- return {
966
- data: block as gloas.BeaconBlock,
967
- meta: {
968
- version: fork,
969
- consensusBlockValue,
970
- },
971
- };
972
- } finally {
973
- timer?.({source});
997
+ if (bestResult === null || bestResult.status !== "fulfilled") {
998
+ const engineReason = engineResult.status === "rejected" ? engineResult.reason : undefined;
999
+ const bidReason = builderBid !== null && bidResult.status === "rejected" ? bidResult.reason : undefined;
1000
+ logger.error("Block production failed", {...logCtx, engineReason, bidReason});
1001
+ throw Error(`Block production failed: engine=${engineReason ?? "n/a"} builder=${bidReason ?? "n/a"}`);
1002
+ }
1003
+
1004
+ const {block, executionPayloadValue, consensusBlockValue} = bestResult.value;
1005
+
1006
+ metrics?.blockProductionSuccess.inc({source});
1007
+ metrics?.blockProductionNumAggregated.observe({source}, block.body.attestations.length);
1008
+ metrics?.blockProductionConsensusBlockValue.observe({source}, Number(formatWeiToEth(consensusBlockValue)));
1009
+ metrics?.blockProductionExecutionPayloadValue.observe({source}, Number(formatWeiToEth(executionPayloadValue)));
1010
+
1011
+ const blockRoot = toRootHex(config.getForkTypes(slot).BeaconBlock.hashTreeRoot(block));
1012
+ logger.verbose("Produced block", {
1013
+ ...logCtx,
1014
+ executionPayloadValue,
1015
+ consensusBlockValue,
1016
+ root: blockRoot,
1017
+ });
1018
+ if (chain.opts.persistProducedBlocks) {
1019
+ void chain.persistBlock(block, "produced_engine_block");
974
1020
  }
1021
+
1022
+ return {
1023
+ data: block as gloas.BeaconBlock,
1024
+ meta: {version: fork, consensusBlockValue},
1025
+ };
975
1026
  },
976
1027
 
977
1028
  async produceAttestationData({committeeIndex, slot}) {
@@ -1059,15 +1110,34 @@ export function getValidatorApi(
1059
1110
  notWhileSyncing();
1060
1111
  await waitForSlot(slot);
1061
1112
 
1062
- const block = chain.forkChoice.getCanonicalBlockClosestLteSlot(slot);
1113
+ const block = chain.forkChoice.getCanonicalBlockAtSlot(slot);
1063
1114
  if (!block) {
1064
- throw new ApiError(404, `No canonical block found at or before slot=${slot}`);
1115
+ // No block is seen at slot. Return 404 so vc can skip casting payload attestation.
1116
+ throw new ApiError(404, `No canonical block found at slot=${slot}`);
1065
1117
  }
1066
1118
 
1067
- const blockIsForSlot = block.slot === slot;
1068
1119
  const payloadInput = chain.seenPayloadEnvelopeInputCache.get(block.blockRoot);
1069
- const payloadPresent = blockIsForSlot && (payloadInput?.hasPayloadEnvelope() ?? false);
1070
- const blobDataAvailable = blockIsForSlot && (payloadInput?.hasAllData() ?? false);
1120
+ // Spec: set payload_present only if the envelope was seen before get_payload_due_ms()
1121
+ // into the slot. Use the envelope's own arrival time (getPayloadEnvelopeSource), not
1122
+ // the input's creation time.
1123
+ const payloadDueSec = config.getPayloadDueMs() / 1000;
1124
+ const payloadSeenSec =
1125
+ payloadInput?.hasPayloadEnvelope() === true
1126
+ ? chain.clock.secFromSlot(slot, payloadInput.getPayloadEnvelopeSource().seenTimestampSec)
1127
+ : null;
1128
+ const payloadPresent = payloadSeenSec !== null && payloadSeenSec < payloadDueSec;
1129
+ const blobDataAvailable = payloadInput?.hasAllData() === true;
1130
+
1131
+ logger.debug("Produced payload attestation data", {
1132
+ slot,
1133
+ blockRoot: block.blockRoot,
1134
+ blockSlot: block.slot,
1135
+ payloadPresent,
1136
+ blobDataAvailable,
1137
+ hasPayloadInput: payloadInput !== undefined,
1138
+ payloadSeenSec,
1139
+ payloadDueSec,
1140
+ });
1071
1141
 
1072
1142
  return {
1073
1143
  data: {
@@ -1124,26 +1194,33 @@ export function getValidatorApi(
1124
1194
  async getProposerDuties({epoch}, _context, opts?: {v2?: boolean}) {
1125
1195
  notWhileSyncing();
1126
1196
 
1127
- // Early check that epoch is no more than current_epoch + 1, or allow for pre-genesis
1128
1197
  const currentEpoch = currentEpochWithDisparity();
1129
1198
  const nextEpoch = currentEpoch + 1;
1130
- if (currentEpoch >= 0 && epoch > nextEpoch) {
1199
+ const startSlot = computeStartSlotAtEpoch(epoch);
1200
+ const prepareNextSlotLookAheadMs =
1201
+ config.SLOT_DURATION_MS - config.getSlotComponentDurationMs(PREPARE_NEXT_SLOT_BPS);
1202
+ const toNextEpochMs = msToNextEpoch();
1203
+ const nearNextEpoch = toNextEpochMs < prepareNextSlotLookAheadMs;
1204
+ // Post-Fulu the proposer lookahead is deterministic and known a full epoch ahead, so
1205
+ // close to the boundary `currentEpoch + 2` is serveable from the upcoming-epoch
1206
+ // checkpoint state (its `nextProposers`). Pre-Fulu / mid-epoch: `currentEpoch + 1` max.
1207
+ const isPostFulu = isForkPostFulu(config.getForkName(startSlot));
1208
+ const maxFutureEpoch = isPostFulu && nearNextEpoch && opts?.v2 ? nextEpoch + 1 : nextEpoch;
1209
+ if (currentEpoch >= 0 && epoch > maxFutureEpoch) {
1131
1210
  throw new ApiError(400, `Requested epoch ${epoch} must not be more than one epoch in the future`);
1132
1211
  }
1133
1212
 
1134
1213
  const head = chain.forkChoice.getHead();
1135
1214
  let state: IBeaconStateView | undefined = undefined;
1136
- const startSlot = computeStartSlotAtEpoch(epoch);
1137
- const prepareNextSlotLookAheadMs =
1138
- config.SLOT_DURATION_MS - config.getSlotComponentDurationMs(PREPARE_NEXT_SLOT_BPS);
1139
- const toNextEpochMs = msToNextEpoch();
1140
1215
  // validators may request next epoch's duties when it's close to next epoch
1141
- // this is to avoid missed block proposal due to 0 epoch look ahead
1142
- if (epoch === nextEpoch && toNextEpochMs < prepareNextSlotLookAheadMs) {
1216
+ // this is to avoid missed block proposal due to 0 epoch look ahead.
1217
+ // Post-Fulu, `nextEpoch + 1` is served from the same upcoming-epoch (`nextEpoch`)
1218
+ // checkpoint state via its `nextProposers` (deterministic proposer lookahead).
1219
+ if (nearNextEpoch && (epoch === nextEpoch || (isPostFulu && epoch === nextEpoch + 1))) {
1143
1220
  // wait for maximum 1 slot for cp state which is the timeout of validator api
1144
1221
  const cpState = await waitForCheckpointState({
1145
1222
  rootHex: head.blockRoot,
1146
- epoch,
1223
+ epoch: nextEpoch,
1147
1224
  });
1148
1225
  if (cpState) {
1149
1226
  state = cpState;
@@ -1218,7 +1295,7 @@ export function getValidatorApi(
1218
1295
  // It should be set to the latest block applied to `self` or the genesis block root.
1219
1296
  const dependentRoot =
1220
1297
  // In v2 the dependent root is different after fulu due to deterministic proposer lookahead
1221
- proposerShufflingDecisionRoot(opts?.v2 ? config.getForkName(startSlot) : ForkName.phase0, state) ||
1298
+ proposerShufflingDecisionRoot(opts?.v2 ? config.getForkName(startSlot) : ForkName.phase0, state, epoch) ||
1222
1299
  (await getGenesisBlockRoot(state));
1223
1300
 
1224
1301
  return {
@@ -116,13 +116,19 @@ export async function importBlock(
116
116
  }
117
117
  executionStatus = parentBlock.executionStatus;
118
118
  }
119
+
120
+ // getBeaconProposerOrNull will return null if head state is more than one epoch away
121
+ // from block slot. We skip proposer boost canonical check as we cannot determine the canonical proposer
122
+ const expectedProposerIndex: number | null = this.getHeadState().getBeaconProposerOrNull(blockSlot);
123
+
119
124
  const blockSummary = this.forkChoice.onBlock(
120
125
  block.message,
121
126
  postState,
122
127
  blockDelaySec,
123
128
  currentSlot,
124
129
  executionStatus,
125
- dataAvailabilityStatus
130
+ dataAvailabilityStatus,
131
+ expectedProposerIndex
126
132
  );
127
133
 
128
134
  // This adds the state necessary to process the next block
@@ -257,8 +263,10 @@ export async function importBlock(
257
263
  if (ptcIndices.length > 0) {
258
264
  this.forkChoice.notifyPtcMessages(
259
265
  toRootHex(payloadAttestation.data.beaconBlockRoot),
266
+ payloadAttestation.data.slot,
260
267
  ptcIndices,
261
- payloadAttestation.data.payloadPresent
268
+ payloadAttestation.data.payloadPresent,
269
+ payloadAttestation.data.blobDataAvailable
262
270
  );
263
271
  }
264
272
  } catch (e) {
@@ -129,7 +129,7 @@ export async function importExecutionPayload(
129
129
 
130
130
  // 3. Regenerate state for envelope verification
131
131
  const blockState = await this.regen
132
- .getBlockSlotState(protoBlock, protoBlock.slot, {dontTransferCache: true}, RegenCaller.processBlock)
132
+ .getBlockSlotState(protoBlock, protoBlock.slot, {dontTransferCache: true}, RegenCaller.importExecutionPayload)
133
133
  .catch(() =>
134
134
  // only happen at the 1st batch of skipped slot checkpoint sync
135
135
  this.regen.getClosestHeadState(protoBlock)
@@ -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
 
@@ -88,6 +88,7 @@ import {
88
88
  ExecutionPayloadBidPool,
89
89
  OpPool,
90
90
  PayloadAttestationPool,
91
+ ProposerPreferencesPool,
91
92
  SyncCommitteeMessagePool,
92
93
  SyncContributionAndProofPool,
93
94
  } from "./opPools/index.js";
@@ -180,6 +181,7 @@ export class BeaconChain implements IBeaconChain {
180
181
  readonly syncContributionAndProofPool;
181
182
  readonly executionPayloadBidPool: ExecutionPayloadBidPool;
182
183
  readonly payloadAttestationPool: PayloadAttestationPool;
184
+ readonly proposerPreferencesPool = new ProposerPreferencesPool();
183
185
  readonly opPool: OpPool;
184
186
 
185
187
  // Gossip seen cache
@@ -1047,6 +1049,7 @@ export class BeaconChain implements IBeaconChain {
1047
1049
  feeRecipient,
1048
1050
  commonBlockBodyPromise,
1049
1051
  parentBlock,
1052
+ builderBid,
1050
1053
  }: BlockAttributes & {commonBlockBodyPromise: Promise<CommonBlockBody>}
1051
1054
  ): Promise<{
1052
1055
  block: AssembledBlockType<T>;
@@ -1076,6 +1079,7 @@ export class BeaconChain implements IBeaconChain {
1076
1079
  proposerIndex,
1077
1080
  proposerPubKey,
1078
1081
  commonBlockBodyPromise,
1082
+ builderBid,
1079
1083
  }
1080
1084
  );
1081
1085
 
@@ -1462,6 +1466,7 @@ export class BeaconChain implements IBeaconChain {
1462
1466
  this.executionPayloadBidPool.prune(slot);
1463
1467
  this.seenExecutionPayloadBids.prune(slot);
1464
1468
  this.seenProposerPreferences.prune(slot);
1469
+ this.proposerPreferencesPool.prune(slot);
1465
1470
  this.seenAttestationDatas.onSlot(slot);
1466
1471
  this.reprocessController.onSlot(slot);
1467
1472
 
@@ -9,8 +9,13 @@ 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",
14
+ NOT_LATER_THAN_PARENT = "EXECUTION_PAYLOAD_BID_ERROR_NOT_LATER_THAN_PARENT",
13
15
  INVALID_SIGNATURE = "EXECUTION_PAYLOAD_BID_ERROR_INVALID_SIGNATURE",
16
+ NO_MATCHING_PROPOSER_PREFERENCES = "EXECUTION_PAYLOAD_BID_ERROR_NO_MATCHING_PROPOSER_PREFERENCES",
17
+ PROPOSER_PREFERENCES_FEE_RECIPIENT_MISMATCH = "EXECUTION_PAYLOAD_BID_ERROR_PROPOSER_PREFERENCES_FEE_RECIPIENT_MISMATCH",
18
+ PROPOSER_PREFERENCES_GAS_LIMIT_MISMATCH = "EXECUTION_PAYLOAD_BID_ERROR_PROPOSER_PREFERENCES_GAS_LIMIT_MISMATCH",
14
19
  }
15
20
 
16
21
  export type ExecutionPayloadBidErrorType =
@@ -35,7 +40,28 @@ export type ExecutionPayloadBidErrorType =
35
40
  commitmentLimit: number;
36
41
  }
37
42
  | {code: ExecutionPayloadBidErrorCode.UNKNOWN_BLOCK_ROOT; parentBlockRoot: RootHex}
43
+ | {code: ExecutionPayloadBidErrorCode.UNKNOWN_PARENT_BLOCK_HASH; parentBlockHash: RootHex}
38
44
  | {code: ExecutionPayloadBidErrorCode.INVALID_SLOT; builderIndex: BuilderIndex; slot: Slot}
39
- | {code: ExecutionPayloadBidErrorCode.INVALID_SIGNATURE; builderIndex: BuilderIndex; slot: Slot};
45
+ | {code: ExecutionPayloadBidErrorCode.NOT_LATER_THAN_PARENT; parentSlot: Slot; slot: Slot}
46
+ | {code: ExecutionPayloadBidErrorCode.INVALID_SIGNATURE; builderIndex: BuilderIndex; slot: Slot}
47
+ | {
48
+ code: ExecutionPayloadBidErrorCode.NO_MATCHING_PROPOSER_PREFERENCES;
49
+ slot: Slot;
50
+ parentBlockRoot: RootHex;
51
+ dependentRoot: RootHex;
52
+ }
53
+ | {
54
+ code: ExecutionPayloadBidErrorCode.PROPOSER_PREFERENCES_FEE_RECIPIENT_MISMATCH;
55
+ builderIndex: BuilderIndex;
56
+ bidFeeRecipient: string;
57
+ expectedFeeRecipient: string;
58
+ }
59
+ | {
60
+ code: ExecutionPayloadBidErrorCode.PROPOSER_PREFERENCES_GAS_LIMIT_MISMATCH;
61
+ builderIndex: BuilderIndex;
62
+ bidGasLimit: number;
63
+ parentGasLimit: number;
64
+ targetGasLimit: number;
65
+ };
40
66
 
41
67
  export class ExecutionPayloadBidError extends GossipActionError<ExecutionPayloadBidErrorType> {}
@@ -5,6 +5,7 @@ export enum PayloadAttestationErrorCode {
5
5
  NOT_CURRENT_SLOT = "PAYLOAD_ATTESTATION_ERROR_NOT_CURRENT_SLOT",
6
6
  PAYLOAD_ATTESTATION_ALREADY_KNOWN = "PAYLOAD_ATTESTATION_ERROR_PAYLOAD_ATTESTATION_ALREADY_KNOWN",
7
7
  UNKNOWN_BLOCK_ROOT = "PAYLOAD_ATTESTATION_ERROR_UNKNOWN_BLOCK_ROOT",
8
+ INVALID_BLOCK_SLOT = "PAYLOAD_ATTESTATION_ERROR_INVALID_BLOCK_SLOT",
8
9
  INVALID_BLOCK = "PAYLOAD_ATTESTATION_ERROR_INVALID_BLOCK",
9
10
  INVALID_ATTESTER = "PAYLOAD_ATTESTATION_ERROR_INVALID_ATTESTER",
10
11
  INVALID_SIGNATURE = "PAYLOAD_ATTESTATION_ERROR_INVALID_SIGNATURE",
@@ -18,6 +19,7 @@ export type PayloadAttestationErrorType =
18
19
  blockRoot: RootHex;
19
20
  }
20
21
  | {code: PayloadAttestationErrorCode.UNKNOWN_BLOCK_ROOT; blockRoot: RootHex}
22
+ | {code: PayloadAttestationErrorCode.INVALID_BLOCK_SLOT; blockRoot: RootHex; blockSlot: Slot; slot: Slot}
21
23
  | {code: PayloadAttestationErrorCode.INVALID_BLOCK; blockRoot: RootHex}
22
24
  | {code: PayloadAttestationErrorCode.INVALID_ATTESTER; attesterIndex: ValidatorIndex}
23
25
  | {code: PayloadAttestationErrorCode.INVALID_SIGNATURE};
@@ -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}),
@@ -47,6 +47,7 @@ import {
47
47
  ExecutionPayloadBidPool,
48
48
  OpPool,
49
49
  PayloadAttestationPool,
50
+ ProposerPreferencesPool,
50
51
  SyncCommitteeMessagePool,
51
52
  SyncContributionAndProofPool,
52
53
  } from "./opPools/index.js";
@@ -124,6 +125,7 @@ export interface IBeaconChain {
124
125
  readonly syncContributionAndProofPool: SyncContributionAndProofPool;
125
126
  readonly executionPayloadBidPool: ExecutionPayloadBidPool;
126
127
  readonly payloadAttestationPool: PayloadAttestationPool;
128
+ readonly proposerPreferencesPool: ProposerPreferencesPool;
127
129
  readonly opPool: OpPool;
128
130
 
129
131
  // Gossip seen cache
@@ -1,12 +1,6 @@
1
1
  import {BitArray} from "@chainsafe/ssz";
2
2
  import {routes} from "@lodestar/api";
3
3
  import {ChainForkConfig} from "@lodestar/config";
4
- import {
5
- LightClientUpdateSummary,
6
- isBetterUpdate,
7
- toLightClientUpdateSummary,
8
- upgradeLightClientHeader,
9
- } from "@lodestar/light-client/spec";
10
4
  import {
11
5
  ForkName,
12
6
  ForkPostAltair,
@@ -27,6 +21,12 @@ import {
27
21
  computeSyncPeriodAtSlot,
28
22
  executionPayloadToPayloadHeader,
29
23
  } from "@lodestar/state-transition";
24
+ import {
25
+ LightClientUpdateSummary,
26
+ isBetterUpdate,
27
+ toLightClientUpdateSummary,
28
+ upgradeLightClientHeader,
29
+ } from "@lodestar/state-transition/light-client";
30
30
  import {
31
31
  BeaconBlock,
32
32
  BeaconBlockBody,
@@ -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
  }
@@ -3,5 +3,6 @@ export {AttestationPool} from "./attestationPool.js";
3
3
  export {ExecutionPayloadBidPool} from "./executionPayloadBidPool.js";
4
4
  export {OpPool} from "./opPool.js";
5
5
  export {PayloadAttestationPool} from "./payloadAttestationPool.js";
6
+ export {ProposerPreferencesPool} from "./proposerPreferencesPool.js";
6
7
  export {SyncCommitteeMessagePool} from "./syncCommitteeMessagePool.js";
7
8
  export {SyncContributionAndProofPool} from "./syncContributionAndProofPool.js";
@@ -57,7 +57,7 @@ export class PayloadAttestationPool {
57
57
  add(
58
58
  message: gloas.PayloadAttestationMessage,
59
59
  payloadAttDataRootHex: RootHex,
60
- validatorCommitteeIndex: number
60
+ validatorCommitteeIndices: number[]
61
61
  ): InsertOutcome {
62
62
  const slot = message.data.slot;
63
63
  const lowestPermissibleSlot = this.lowestPermissibleSlot;
@@ -85,10 +85,10 @@ export class PayloadAttestationPool {
85
85
  const aggregate = aggregateByDataRoot.get(payloadAttDataRootHex);
86
86
  if (aggregate) {
87
87
  // Aggregate msg into aggregate
88
- return aggregateMessageInto(message, validatorCommitteeIndex, aggregate);
88
+ return aggregateMessageInto(message, validatorCommitteeIndices, aggregate);
89
89
  }
90
90
  // Create a new aggregate with data
91
- aggregateByDataRoot.set(payloadAttDataRootHex, messageToAggregate(message, validatorCommitteeIndex));
91
+ aggregateByDataRoot.set(payloadAttDataRootHex, messageToAggregate(message, validatorCommitteeIndices));
92
92
 
93
93
  return InsertOutcome.NewData;
94
94
  }
@@ -150,25 +150,49 @@ export class PayloadAttestationPool {
150
150
  }
151
151
  }
152
152
 
153
- function messageToAggregate(message: gloas.PayloadAttestationMessage, validatorCommitteeIndex: number): AggregateFast {
153
+ function messageToAggregate(
154
+ message: gloas.PayloadAttestationMessage,
155
+ validatorCommitteeIndices: number[]
156
+ ): AggregateFast {
157
+ const aggregationBits = BitArray.fromBitLen(PTC_SIZE);
158
+ for (const index of validatorCommitteeIndices) {
159
+ aggregationBits.set(index, true);
160
+ }
161
+ const sig = signatureFromBytesNoCheck(message.signature);
162
+ // The validator signed once but occupies `validatorCommitteeIndices.length` PTC positions.
163
+ // Verification aggregates the pubkey once per set bit, so the signature must be aggregated
164
+ // the same number of times for the BLS check to balance — same pattern as sync committee.
165
+ const signature =
166
+ validatorCommitteeIndices.length === 1
167
+ ? sig
168
+ : aggregateSignatures(new Array(validatorCommitteeIndices.length).fill(sig));
154
169
  return {
155
- aggregationBits: BitArray.fromSingleBit(PTC_SIZE, validatorCommitteeIndex),
170
+ aggregationBits,
156
171
  data: message.data,
157
- signature: signatureFromBytesNoCheck(message.signature),
172
+ signature,
158
173
  };
159
174
  }
160
175
 
161
176
  function aggregateMessageInto(
162
177
  message: gloas.PayloadAttestationMessage,
163
- validatorCommitteeIndex: number,
178
+ validatorCommitteeIndices: number[],
164
179
  aggregate: AggregateFast
165
180
  ): InsertOutcome {
166
- if (aggregate.aggregationBits.get(validatorCommitteeIndex) === true) {
181
+ // Gossip dedup via `seenPayloadAttesters` is keyed by (epoch, validatorIndex), so the same
182
+ // validator's message is never processed twice — all of its bits are set together or none.
183
+ // Checking the first index is sufficient.
184
+ if (aggregate.aggregationBits.get(validatorCommitteeIndices[0]) === true) {
167
185
  return InsertOutcome.AlreadyKnown;
168
186
  }
169
187
 
170
- aggregate.aggregationBits.set(validatorCommitteeIndex, true);
171
- aggregate.signature = aggregateSignatures([aggregate.signature, signatureFromBytesNoCheck(message.signature)]);
188
+ for (const index of validatorCommitteeIndices) {
189
+ aggregate.aggregationBits.set(index, true);
190
+ }
191
+ const sig = signatureFromBytesNoCheck(message.signature);
192
+ aggregate.signature = aggregateSignatures([
193
+ aggregate.signature,
194
+ ...new Array(validatorCommitteeIndices.length).fill(sig),
195
+ ]);
172
196
 
173
197
  return InsertOutcome.Aggregated;
174
198
  }