@lodestar/beacon-node 1.46.0 → 1.47.0-dev.09607ce6d4

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 (106) hide show
  1. package/lib/api/impl/beacon/blocks/index.d.ts.map +1 -1
  2. package/lib/api/impl/beacon/blocks/index.js +3 -6
  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 +4 -0
  6. package/lib/api/impl/beacon/pool/index.js.map +1 -1
  7. package/lib/api/impl/lodestar/index.d.ts.map +1 -1
  8. package/lib/api/impl/lodestar/index.js +11 -12
  9. package/lib/api/impl/lodestar/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 +12 -22
  12. package/lib/api/impl/validator/index.js.map +1 -1
  13. package/lib/api/impl/validator/utils.d.ts +5 -0
  14. package/lib/api/impl/validator/utils.d.ts.map +1 -1
  15. package/lib/api/impl/validator/utils.js +18 -12
  16. package/lib/api/impl/validator/utils.js.map +1 -1
  17. package/lib/chain/archiveStore/historicalState/metrics.d.ts.map +1 -1
  18. package/lib/chain/archiveStore/historicalState/metrics.js +21 -0
  19. package/lib/chain/archiveStore/historicalState/metrics.js.map +1 -1
  20. package/lib/chain/blocks/importBlock.d.ts.map +1 -1
  21. package/lib/chain/blocks/importBlock.js +3 -8
  22. package/lib/chain/blocks/importBlock.js.map +1 -1
  23. package/lib/chain/blocks/importExecutionPayload.js +1 -1
  24. package/lib/chain/blocks/importExecutionPayload.js.map +1 -1
  25. package/lib/chain/blocks/index.d.ts.map +1 -1
  26. package/lib/chain/blocks/index.js +3 -5
  27. package/lib/chain/blocks/index.js.map +1 -1
  28. package/lib/chain/blocks/verifyBlock.d.ts.map +1 -1
  29. package/lib/chain/blocks/verifyBlock.js +11 -16
  30. package/lib/chain/blocks/verifyBlock.js.map +1 -1
  31. package/lib/chain/builderCircuitBreaker.d.ts +6 -5
  32. package/lib/chain/builderCircuitBreaker.d.ts.map +1 -1
  33. package/lib/chain/builderCircuitBreaker.js +28 -15
  34. package/lib/chain/builderCircuitBreaker.js.map +1 -1
  35. package/lib/chain/chain.d.ts +1 -1
  36. package/lib/chain/chain.d.ts.map +1 -1
  37. package/lib/chain/chain.js +8 -2
  38. package/lib/chain/chain.js.map +1 -1
  39. package/lib/chain/errors/blockError.d.ts +1 -0
  40. package/lib/chain/errors/blockError.d.ts.map +1 -1
  41. package/lib/chain/options.d.ts +1 -3
  42. package/lib/chain/options.d.ts.map +1 -1
  43. package/lib/chain/options.js.map +1 -1
  44. package/lib/chain/prepareNextSlot.js +2 -2
  45. package/lib/chain/prepareNextSlot.js.map +1 -1
  46. package/lib/chain/produceBlock/produceBlockBody.d.ts +2 -0
  47. package/lib/chain/produceBlock/produceBlockBody.d.ts.map +1 -1
  48. package/lib/chain/produceBlock/produceBlockBody.js +8 -3
  49. package/lib/chain/produceBlock/produceBlockBody.js.map +1 -1
  50. package/lib/chain/seenCache/seenBlockProposers.d.ts +5 -1
  51. package/lib/chain/seenCache/seenBlockProposers.d.ts.map +1 -1
  52. package/lib/chain/seenCache/seenBlockProposers.js +17 -7
  53. package/lib/chain/seenCache/seenBlockProposers.js.map +1 -1
  54. package/lib/chain/validation/block.d.ts.map +1 -1
  55. package/lib/chain/validation/block.js +21 -7
  56. package/lib/chain/validation/block.js.map +1 -1
  57. package/lib/execution/builder/http.d.ts +10 -7
  58. package/lib/execution/builder/http.d.ts.map +1 -1
  59. package/lib/execution/builder/http.js +11 -8
  60. package/lib/execution/builder/http.js.map +1 -1
  61. package/lib/metrics/metrics/beacon.d.ts +3 -3
  62. package/lib/metrics/metrics/beacon.d.ts.map +1 -1
  63. package/lib/metrics/metrics/beacon.js +9 -9
  64. package/lib/metrics/metrics/beacon.js.map +1 -1
  65. package/lib/metrics/metrics/lodestar.d.ts +10 -0
  66. package/lib/metrics/metrics/lodestar.d.ts.map +1 -1
  67. package/lib/metrics/metrics/lodestar.js +16 -0
  68. package/lib/metrics/metrics/lodestar.js.map +1 -1
  69. package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
  70. package/lib/network/processor/gossipHandlers.js +32 -5
  71. package/lib/network/processor/gossipHandlers.js.map +1 -1
  72. package/lib/sync/range/batch.js +1 -1
  73. package/lib/sync/range/batch.js.map +1 -1
  74. package/lib/sync/range/utils/hashBlocks.d.ts +16 -4
  75. package/lib/sync/range/utils/hashBlocks.d.ts.map +1 -1
  76. package/lib/sync/range/utils/hashBlocks.js +49 -18
  77. package/lib/sync/range/utils/hashBlocks.js.map +1 -1
  78. package/lib/sync/unknownBlock.d.ts.map +1 -1
  79. package/lib/sync/unknownBlock.js +17 -5
  80. package/lib/sync/unknownBlock.js.map +1 -1
  81. package/package.json +16 -16
  82. package/src/api/impl/beacon/blocks/index.ts +4 -6
  83. package/src/api/impl/beacon/pool/index.ts +5 -0
  84. package/src/api/impl/lodestar/index.ts +11 -12
  85. package/src/api/impl/validator/index.ts +22 -27
  86. package/src/api/impl/validator/utils.ts +30 -15
  87. package/src/chain/archiveStore/historicalState/metrics.ts +22 -0
  88. package/src/chain/blocks/importBlock.ts +2 -7
  89. package/src/chain/blocks/importExecutionPayload.ts +1 -1
  90. package/src/chain/blocks/index.ts +3 -5
  91. package/src/chain/blocks/verifyBlock.ts +21 -25
  92. package/src/chain/builderCircuitBreaker.ts +31 -17
  93. package/src/chain/chain.ts +8 -1
  94. package/src/chain/errors/blockError.ts +1 -1
  95. package/src/chain/options.ts +1 -3
  96. package/src/chain/prepareNextSlot.ts +2 -2
  97. package/src/chain/produceBlock/produceBlockBody.ts +15 -2
  98. package/src/chain/seenCache/seenBlockProposers.ts +24 -7
  99. package/src/chain/validation/block.ts +21 -7
  100. package/src/execution/builder/http.ts +11 -11
  101. package/src/metrics/metrics/beacon.ts +9 -9
  102. package/src/metrics/metrics/lodestar.ts +16 -0
  103. package/src/network/processor/gossipHandlers.ts +35 -3
  104. package/src/sync/range/batch.ts +1 -1
  105. package/src/sync/range/utils/hashBlocks.ts +56 -21
  106. package/src/sync/unknownBlock.ts +18 -5
@@ -220,12 +220,6 @@ export function getBeaconBlockApi({
220
220
  // Block has already been seen, e.g. via gossip racing the publish API. Benign.
221
221
  chain.logger.debug("Ignoring already-known block during publishing", valLogMeta);
222
222
  return;
223
- case BlockErrorCode.REPEAT_PROPOSAL:
224
- // The proposer already produced a block for this slot. For a solo setup this is a
225
- // notable signal (duplicate-proposal attempt). For fallback / DVT setups it is
226
- // expected on every block where another node published first.
227
- chain.logger.warn("Ignoring repeat-proposal block during publishing", valLogMeta);
228
- return;
229
223
  }
230
224
  }
231
225
 
@@ -1032,6 +1026,7 @@ export function getBeaconBlockApi({
1032
1026
  },
1033
1027
 
1034
1028
  async publishExecutionPayloadBid({signedExecutionPayloadBid}) {
1029
+ const seenTimestampSec = Date.now() / 1000;
1035
1030
  const bid = signedExecutionPayloadBid.message;
1036
1031
  const slot = bid.slot;
1037
1032
  const fork = config.getForkName(slot);
@@ -1042,6 +1037,9 @@ export function getBeaconBlockApi({
1042
1037
 
1043
1038
  await validateApiExecutionPayloadBid(chain, signedExecutionPayloadBid);
1044
1039
 
1040
+ const elapsedSec = chain.clock.secFromSlot(slot, seenTimestampSec);
1041
+ metrics?.gossipExecutionPayloadBid.elapsedTimeTillReceived.observe({source: OpSource.api}, elapsedSec);
1042
+
1045
1043
  try {
1046
1044
  const insertOutcome = chain.executionPayloadBidPool.add(signedExecutionPayloadBid);
1047
1045
  metrics?.opPool.executionPayloadBidPool.apiInsertOutcome.inc({insertOutcome});
@@ -25,6 +25,7 @@ import {validateApiPayloadAttestationMessage} from "../../../../chain/validation
25
25
  import {validateApiProposerSlashing} from "../../../../chain/validation/proposerSlashing.js";
26
26
  import {validateApiSyncCommittee} from "../../../../chain/validation/syncCommittee.js";
27
27
  import {validateApiVoluntaryExit} from "../../../../chain/validation/voluntaryExit.js";
28
+ import {OpSource} from "../../../../chain/validatorMonitor.js";
28
29
  import {validateGossipFnRetryUnknownRoot} from "../../../../network/processor/gossipHandlers.js";
29
30
  import {ApiError, FailureList, IndexedError} from "../../errors.js";
30
31
  import {ApiModules} from "../../types.js";
@@ -211,6 +212,7 @@ export function getBeaconPoolApi({
211
212
  },
212
213
 
213
214
  async submitPayloadAttestationMessages({payloadAttestationMessages}) {
215
+ const seenTimestampSec = Date.now() / 1000;
214
216
  const failures: FailureList = [];
215
217
 
216
218
  await Promise.all(
@@ -226,6 +228,9 @@ export function getBeaconPoolApi({
226
228
  beaconBlockRoot
227
229
  );
228
230
 
231
+ const delaySec = chain.clock.secFromSlot(slot, seenTimestampSec);
232
+ metrics?.gossipPayloadAttestationMessage.elapsedTimeTillReceived.observe({source: OpSource.api}, delaySec);
233
+
229
234
  const insertOutcome = chain.payloadAttestationPool.add(
230
235
  payloadAttestationMessage,
231
236
  attDataRootHex,
@@ -280,7 +280,7 @@ export function getLodestarApi({
280
280
  },
281
281
 
282
282
  async getFastConfirmationInfo() {
283
- const confirmedRoot = chain.forkChoice.getConfirmedRoot();
283
+ const fcrStore = chain.forkChoice.getFastConfirmationStore();
284
284
  const confirmedBlock = chain.forkChoice.getConfirmedBlock();
285
285
  const justifiedCheckpoint = chain.forkChoice.getJustifiedCheckpoint();
286
286
  const finalizedCheckpoint = chain.forkChoice.getFinalizedCheckpoint();
@@ -290,21 +290,20 @@ export function getLodestarApi({
290
290
  return {
291
291
  data: {
292
292
  confirmed: {
293
- rootHex: confirmedRoot,
294
- slot: confirmedBlock?.slot ?? null,
293
+ root: fromHex(fcrStore.confirmedRoot),
294
+ slot: confirmedBlock?.slot ?? 0,
295
295
  },
296
296
  head: {
297
- rootHex: headRoot,
297
+ root: fromHex(headRoot),
298
298
  slot: head.slot,
299
299
  },
300
- justifiedCheckpoint: {
301
- rootHex: justifiedCheckpoint.rootHex,
302
- epoch: justifiedCheckpoint.epoch,
303
- },
304
- finalizedCheckpoint: {
305
- rootHex: finalizedCheckpoint.rootHex,
306
- epoch: finalizedCheckpoint.epoch,
307
- },
300
+ justifiedCheckpoint,
301
+ finalizedCheckpoint,
302
+ previousEpochObservedJustifiedCheckpoint: fcrStore.previousEpochObservedJustifiedCheckpoint,
303
+ currentEpochObservedJustifiedCheckpoint: fcrStore.currentEpochObservedJustifiedCheckpoint,
304
+ previousEpochGreatestUnrealizedCheckpoint: fcrStore.previousEpochGreatestUnrealizedCheckpoint,
305
+ previousSlotHead: fromHex(fcrStore.previousSlotHead),
306
+ currentSlotHead: fromHex(fcrStore.currentSlotHead),
308
307
  },
309
308
  };
310
309
  },
@@ -92,7 +92,12 @@ import {getStateResponseWithRegen} from "../beacon/state/utils.js";
92
92
  import {ApiError, FailureList, IndexedError, NodeIsSyncing, OnlySupportedByDVT} from "../errors.js";
93
93
  import {ApiModules} from "../types.js";
94
94
  import {notWhileSyncing} from "../utils.js";
95
- import {computeSubnetForCommitteesAtSlot, getPubkeysForIndices, selectBlockProductionSource} from "./utils.js";
95
+ import {
96
+ computeSubnetForCommitteesAtSlot,
97
+ getPubkeysForIndices,
98
+ selectBlockProductionSource,
99
+ selectBlockProductionSourceByBoostFactor,
100
+ } from "./utils.js";
96
101
 
97
102
  /**
98
103
  * Cutoff time to wait from start of the slot for execution and builder block production apis to resolve.
@@ -577,8 +582,7 @@ export function getValidatorApi(
577
582
  builderSelection,
578
583
  isBuilderEnabled,
579
584
  strictFeeRecipientCheck,
580
- // winston logger doesn't like bigint
581
- builderBoostFactor: `${builderBoostFactor}`,
585
+ builderBoostFactor,
582
586
  };
583
587
 
584
588
  logger.verbose("Assembling block with produceEngineOrBuilderBlock", loggerContext);
@@ -851,8 +855,8 @@ export function getValidatorApi(
851
855
  randaoReveal,
852
856
  graffiti,
853
857
  feeRecipient,
858
+ strictFeeRecipientCheck,
854
859
  includePayload,
855
- builderSelection,
856
860
  builderBoostFactor,
857
861
  }) {
858
862
  const fork = config.getForkName(slot);
@@ -861,11 +865,6 @@ export function getValidatorApi(
861
865
  throw new ApiError(400, `produceBlockV4 not supported for pre-gloas fork=${fork}`);
862
866
  }
863
867
 
864
- builderSelection = builderSelection ?? routes.validator.BuilderSelection.MaxProfit;
865
- if (builderSelection === routes.validator.BuilderSelection.BuilderOnly) {
866
- logger.warn("Builder selection builderonly is no longer supported, treating as builderalways");
867
- builderSelection = routes.validator.BuilderSelection.BuilderAlways;
868
- }
869
868
  builderBoostFactor = builderBoostFactor ?? BigInt(100);
870
869
  if (builderBoostFactor > MAX_BUILDER_BOOST_FACTOR) {
871
870
  throw new ApiError(400, `Invalid builderBoostFactor=${builderBoostFactor} > MAX_BUILDER_BOOST_FACTOR`);
@@ -896,14 +895,11 @@ export function getValidatorApi(
896
895
  // TODO GLOAS: add external builder api support when it is implemented
897
896
  const isBuildingOnFull = chain.forkChoice.shouldBuildOnFull(parentBlock, slot);
898
897
  const bidParentBlockHash = isBuildingOnFull ? parentBlock.executionPayloadBlockHash : parentBlock.parentBlockHash;
899
- // Bids are only skipped entirely with executiononly or while the circuit breaker is active,
900
- // other engine-preferring selections still build a block with the best bid as fallback in
901
- // case local production fails
902
- const circuitBreakerActive = chain.builderCircuitBreaker.isActive(slot);
903
- const builderBid =
904
- builderSelection === routes.validator.BuilderSelection.ExecutionOnly || circuitBreakerActive
905
- ? null
906
- : chain.executionPayloadBidPool.getBestBid(slot, bidParentBlockHash, parentBlockRootHex);
898
+ const circuitBreakerActive = chain.builderCircuitBreaker.isActive(slot, parentBlock);
899
+ // Keep a builder bid as fallback unless the circuit breaker is active
900
+ const builderBid = circuitBreakerActive
901
+ ? null
902
+ : chain.executionPayloadBidPool.getBestBid(slot, bidParentBlockHash, parentBlockRootHex);
907
903
 
908
904
  const logCtx = {
909
905
  slot,
@@ -911,8 +907,8 @@ export function getValidatorApi(
911
907
  parentBlockRoot: parentBlockRootHex,
912
908
  parentBlockHash: parentBlock.executionPayloadBlockHash,
913
909
  fork,
914
- builderSelection,
915
910
  builderBoostFactor,
911
+ strictFeeRecipientCheck,
916
912
  circuitBreakerActive,
917
913
  ...(builderBid !== null
918
914
  ? {
@@ -936,6 +932,7 @@ export function getValidatorApi(
936
932
  randaoReveal,
937
933
  graffiti: graffitiBytes,
938
934
  feeRecipient,
935
+ strictFeeRecipientCheck,
939
936
  commonBlockBodyPromise,
940
937
  };
941
938
 
@@ -960,12 +957,8 @@ export function getValidatorApi(
960
957
  chain.produceBlock(baseAttrs)
961
958
  ).then((engineBlock) => {
962
959
  // No need to wait for the bid block if the engine block will always be selected due to
963
- // suspected builder censorship, a builder boost factor of 0 or executionalways selection
964
- if (
965
- engineBlock.shouldOverrideBuilder ||
966
- builderBoostFactor === BigInt(0) ||
967
- builderSelection === routes.validator.BuilderSelection.ExecutionAlways
968
- ) {
960
+ // suspected builder censorship or a builder boost factor of 0
961
+ if (engineBlock.shouldOverrideBuilder || builderBoostFactor === BigInt(0)) {
969
962
  controller.abort();
970
963
  }
971
964
  return engineBlock;
@@ -994,8 +987,7 @@ export function getValidatorApi(
994
987
  });
995
988
  logger.warn("Selected local block: censorship suspected in builder bid", logCtx);
996
989
  } else if (engineResult.status === "fulfilled" && bidResult.status === "fulfilled") {
997
- const result = selectBlockProductionSource({
998
- builderSelection,
990
+ const result = selectBlockProductionSourceByBoostFactor({
999
991
  builderBoostFactor,
1000
992
  engineExecutionPayloadValue: engineResult.value.executionPayloadValue,
1001
993
  // The bid value is the payment to the proposer, in Gwei
@@ -1061,7 +1053,10 @@ export function getValidatorApi(
1061
1053
  root: blockRoot,
1062
1054
  });
1063
1055
  if (chain.opts.persistProducedBlocks) {
1064
- void chain.persistBlock(block, "produced_engine_block");
1056
+ void chain.persistBlock(
1057
+ block,
1058
+ source === ProducedBlockSource.builder ? "produced_builder_block" : "produced_engine_block"
1059
+ );
1065
1060
  }
1066
1061
 
1067
1062
  // Include the payload for self-builds unless disabled (stateless flow)
@@ -59,24 +59,39 @@ export function selectBlockProductionSource({
59
59
  return {source: ProducedBlockSource.engine, reason: EngineBlockSelectionReason.EnginePreferred};
60
60
 
61
61
  case routes.validator.BuilderSelection.Default:
62
- case routes.validator.BuilderSelection.MaxProfit: {
63
- if (builderBoostFactor === BigInt(0)) {
64
- return {source: ProducedBlockSource.engine, reason: EngineBlockSelectionReason.EnginePreferred};
65
- }
66
-
67
- if (builderBoostFactor === MAX_BUILDER_BOOST_FACTOR) {
68
- return {source: ProducedBlockSource.builder, reason: BuilderBlockSelectionReason.BuilderPreferred};
69
- }
70
-
71
- if (engineExecutionPayloadValue >= (builderExecutionPayloadValue * builderBoostFactor) / BigInt(100)) {
72
- return {source: ProducedBlockSource.engine, reason: EngineBlockSelectionReason.BlockValue};
73
- }
74
-
75
- return {source: ProducedBlockSource.builder, reason: BuilderBlockSelectionReason.BlockValue};
76
- }
62
+ case routes.validator.BuilderSelection.MaxProfit:
63
+ return selectBlockProductionSourceByBoostFactor({
64
+ engineExecutionPayloadValue,
65
+ builderExecutionPayloadValue,
66
+ builderBoostFactor,
67
+ });
77
68
 
78
69
  case routes.validator.BuilderSelection.BuilderAlways:
79
70
  case routes.validator.BuilderSelection.BuilderOnly:
80
71
  return {source: ProducedBlockSource.builder, reason: BuilderBlockSelectionReason.BuilderPreferred};
81
72
  }
82
73
  }
74
+
75
+ export function selectBlockProductionSourceByBoostFactor({
76
+ engineExecutionPayloadValue,
77
+ builderExecutionPayloadValue,
78
+ builderBoostFactor,
79
+ }: {
80
+ engineExecutionPayloadValue: bigint;
81
+ builderExecutionPayloadValue: bigint;
82
+ builderBoostFactor: bigint;
83
+ }): BlockSelectionResult {
84
+ if (builderBoostFactor === BigInt(0)) {
85
+ return {source: ProducedBlockSource.engine, reason: EngineBlockSelectionReason.EnginePreferred};
86
+ }
87
+
88
+ if (builderBoostFactor === MAX_BUILDER_BOOST_FACTOR) {
89
+ return {source: ProducedBlockSource.builder, reason: BuilderBlockSelectionReason.BuilderPreferred};
90
+ }
91
+
92
+ if (engineExecutionPayloadValue >= (builderExecutionPayloadValue * builderBoostFactor) / BigInt(100)) {
93
+ return {source: ProducedBlockSource.engine, reason: EngineBlockSelectionReason.BlockValue};
94
+ }
95
+
96
+ return {source: ProducedBlockSource.builder, reason: BuilderBlockSelectionReason.BlockValue};
97
+ }
@@ -1,3 +1,4 @@
1
+ import {ForkName} from "@lodestar/params";
1
2
  import {
2
3
  BeaconStateTransitionMetrics,
3
4
  EpochTransitionStep,
@@ -45,6 +46,27 @@ export function createHistoricalStateTransitionMetrics(
45
46
  labelNames: ["step"],
46
47
  buckets: [0.01, 0.05, 0.1, 0.2, 0.5, 0.75, 1],
47
48
  }),
49
+ forkUpgradeTime: metricsRegister.histogram<{fork: ForkName}>({
50
+ name: "lodestar_historical_state_stfn_fork_upgrade_seconds",
51
+ help: "Time to upgrade the state at a fork boundary in seconds",
52
+ labelNames: ["fork"],
53
+ buckets: [0.05, 0.1, 0.25, 0.5, 1, 2, 5, 10, 30],
54
+ }),
55
+ onboardBuildersTime: metricsRegister.histogram({
56
+ name: "lodestar_historical_state_stfn_gloas_onboard_builders_seconds",
57
+ help: "Time spent in onboardBuildersFromPendingDeposits at the gloas fork transition",
58
+ buckets: [0.05, 0.1, 0.25, 0.5, 1, 2, 5, 10, 30],
59
+ }),
60
+ onboardBuildersDeposits: metricsRegister.gauge<{outcome: "onboarded" | "topup" | "kept" | "dropped"}>({
61
+ name: "lodestar_historical_state_stfn_gloas_onboard_builders_deposits",
62
+ help: "Pending deposits handled at the gloas fork transition, by outcome",
63
+ labelNames: ["outcome"],
64
+ }),
65
+ onboardBuildersSignatureChecks: metricsRegister.gauge<{source: "cache" | "verified"}>({
66
+ name: "lodestar_historical_state_stfn_gloas_onboard_builders_signature_checks",
67
+ help: "Builder deposit signature checks at the gloas fork transition, by whether the pre-verify cache served them",
68
+ labelNames: ["source"],
69
+ }),
48
70
  processBlockTime: metricsRegister.histogram({
49
71
  name: "lodestar_historical_state_stfn_process_block_seconds",
50
72
  help: "Time to process a single block in seconds",
@@ -22,7 +22,6 @@ import {
22
22
  RootCache,
23
23
  computeEpochAtSlot,
24
24
  computeTimeAtSlot,
25
- isStartSlotOfEpoch,
26
25
  isStatePostAltair,
27
26
  isStatePostBellatrix,
28
27
  } from "@lodestar/state-transition";
@@ -404,11 +403,7 @@ export async function importBlock(
404
403
  notOverrideFcuReason = NotReorgedReason.NotProposerOfNextSlot;
405
404
  }
406
405
  } catch (e) {
407
- if (isStartSlotOfEpoch(proposalSlot)) {
408
- notOverrideFcuReason = NotReorgedReason.AtEpochBoundary;
409
- } else {
410
- this.logger.warn("Unable to get beacon proposer. Do not override fcu.", {proposalSlot}, e as Error);
411
- }
406
+ this.logger.warn("Unable to get beacon proposer. Do not override fcu.", {proposalSlot}, e as Error);
412
407
  }
413
408
 
414
409
  if (shouldOverrideFcu) {
@@ -445,7 +440,7 @@ export async function importBlock(
445
440
  * zero block hash (pre TTD)
446
441
  */
447
442
  const safeBlockHash = getSafeExecutionBlockHash(this.forkChoice, this.logger);
448
- const finalizedBlockHash = getFinalizedExecutionBlockHash(this.forkChoice, this.logger);
443
+ const finalizedBlockHash = getFinalizedExecutionBlockHash(this.forkChoice);
449
444
  if (headBlockHash !== ZERO_HASH_HEX) {
450
445
  this.executionEngine
451
446
  .notifyForkchoiceUpdate(
@@ -250,7 +250,7 @@ export async function importExecutionPayload(
250
250
  const head = this.forkChoice.getHead();
251
251
  if (!this.opts.disableImportExecutionFcU && blockRootHex === head.blockRoot) {
252
252
  const safeBlockHash = getSafeExecutionBlockHash(this.forkChoice, this.logger);
253
- const finalizedBlockHash = getFinalizedExecutionBlockHash(this.forkChoice, this.logger);
253
+ const finalizedBlockHash = getFinalizedExecutionBlockHash(this.forkChoice);
254
254
  this.executionEngine.notifyForkchoiceUpdate(fork, blockHashHex, safeBlockHash, finalizedBlockHash).catch((e) => {
255
255
  if (!isErrorAborted(e) && !isQueueErrorAborted(e)) {
256
256
  this.logger.error("Error pushing notifyForkchoiceUpdate()", {blockHashHex, finalizedBlockHash}, e);
@@ -108,11 +108,9 @@ export async function processBlocks(
108
108
  throw segmentExecStatus.execAborted.execError;
109
109
  }
110
110
 
111
- if (opts.skipVerifyBlockSignatures !== true) {
112
- for (const blockInput of relevantBlocks) {
113
- const block = blockInput.getBlock().message;
114
- this.seenBlockProposers.add(block.slot, block.proposerIndex);
115
- }
111
+ for (const blockInput of relevantBlocks) {
112
+ const block = blockInput.getBlock().message;
113
+ this.seenBlockProposers.add(block.slot, block.proposerIndex, blockInput.blockRootHex);
116
114
  }
117
115
 
118
116
  const {executionStatuses} = segmentExecStatus;
@@ -6,9 +6,9 @@ import {
6
6
  computeEpochAtSlot,
7
7
  signedBlockToSignedHeader,
8
8
  } from "@lodestar/state-transition";
9
- import {IndexedAttestation, Slot, deneb, ssz} from "@lodestar/types";
10
- import {toRootHex} from "@lodestar/utils";
9
+ import {IndexedAttestation, Slot, deneb} from "@lodestar/types";
11
10
  import {getBlobKzgCommitments} from "../../util/dataColumns.js";
11
+ import {callInNextEventLoop} from "../../util/eventLoop.js";
12
12
  import type {BeaconChain} from "../chain.js";
13
13
  import {BlockError, BlockErrorCode} from "../errors/index.js";
14
14
  import {BlockProcessOpts} from "../options.js";
@@ -193,34 +193,30 @@ export async function verifyBlocksInEpoch(
193
193
  ),
194
194
 
195
195
  // All signatures at once
196
- opts.skipVerifyBlockSignatures !== true
197
- ? verifyBlocksSignatures(
198
- this.config,
199
- this.bls,
200
- this.logger,
201
- this.metrics,
202
- preState0,
203
- blocks,
204
- indexedAttestationsByBlock,
205
- opts
206
- )
207
- : Promise.resolve({verifySignaturesTime: Date.now()}),
196
+ verifyBlocksSignatures(
197
+ this.config,
198
+ this.bls,
199
+ this.logger,
200
+ this.metrics,
201
+ preState0,
202
+ blocks,
203
+ indexedAttestationsByBlock,
204
+ opts
205
+ ),
208
206
 
209
207
  // TODO GLOAS: can verify payload signatures in batch too
210
208
  // maybe chain with the above verifyBlocksSignatures()
211
209
  ]);
212
210
 
213
- if (opts.skipVerifyBlockSignatures !== true) {
214
- for (const block of blocks) {
215
- const {slot, proposerIndex} = block.message;
216
- const signedBlockHeader = signedBlockToSignedHeader(this.config, block);
217
- const blockRoot = toRootHex(ssz.phase0.BeaconBlockHeader.hashTreeRoot(signedBlockHeader.message));
218
- this.seenBlockProposers.observeBlockRoot(slot, proposerIndex, blockRoot, signedBlockHeader);
219
- // Only produce a slashing while importing the block. A block that is verified before it is published
220
- // must not be treated as equivocation evidence since it may never be seen by the network
221
- if (opts.verifyOnly !== true && this.seenBlockProposers.isEquivocating(slot, proposerIndex)) {
222
- this.processProposerEquivocation(slot, proposerIndex);
223
- }
211
+ for (const blockInput of blockInputs) {
212
+ const block = blockInput.getBlock();
213
+ const {slot, proposerIndex} = block.message;
214
+ const signedBlockHeader = signedBlockToSignedHeader(this.config, block);
215
+ this.seenBlockProposers.observeBlockRoot(slot, proposerIndex, blockInput.blockRootHex, signedBlockHeader);
216
+ // Only produce a slashing while importing the block. A block that is verified before it is published
217
+ // must not be treated as equivocation evidence since it may never be seen by the network
218
+ if (opts.verifyOnly !== true && this.seenBlockProposers.isEquivocating(slot, proposerIndex)) {
219
+ callInNextEventLoop(() => this.processProposerEquivocation(slot, proposerIndex));
224
220
  }
225
221
  }
226
222
 
@@ -1,4 +1,4 @@
1
- import {IForkChoice} from "@lodestar/fork-choice";
1
+ import {IForkChoice, ProtoBlock} from "@lodestar/fork-choice";
2
2
  import {Slot} from "@lodestar/types";
3
3
  import {Logger} from "@lodestar/utils";
4
4
  import {getFaultInspectionParams} from "../execution/builder/http.js";
@@ -15,11 +15,15 @@ export type BuilderCircuitBreakerModules = {
15
15
  metrics: Metrics | null;
16
16
  };
17
17
 
18
+ /** Four observations is the minimum useful recovery sample for the default ~25% fault budget */
19
+ const MIN_BLOCKS_TO_DEACTIVATE = 4;
20
+
18
21
  /**
19
22
  * Post-gloas circuit breaker for builder bids. The beacon block is produced by the proposer
20
23
  * regardless of bid source, so missed blocks are not a useful builder health signal. Instead
21
- * count blocks whose payload was never revealed and stop selecting builder bids while the
22
- * non-reveal rate in the fault inspection window is too high.
24
+ * count canonical blocks selected as EMPTY. Activate when the proportion of EMPTY blocks exceeds
25
+ * the fault budget, and resume selecting builder bids only when the observed blocks are within
26
+ * budget and meet the minimum recovery sample size.
23
27
  */
24
28
  export class BuilderCircuitBreaker {
25
29
  readonly faultInspectionWindow: number;
@@ -35,39 +39,49 @@ export class BuilderCircuitBreaker {
35
39
  const {faultInspectionWindow, allowedFaults} = getFaultInspectionParams(opts);
36
40
  this.faultInspectionWindow = faultInspectionWindow;
37
41
  this.allowedFaults = allowedFaults;
42
+ this.modules.logger.info("Builder circuit breaker initialized", {faultInspectionWindow, allowedFaults});
38
43
  }
39
44
 
40
45
  /** Whether builder bids must be ignored for a block produced at clockSlot */
41
- isActive(clockSlot: Slot): boolean {
42
- this.update(clockSlot);
46
+ isActive(clockSlot: Slot, head: ProtoBlock): boolean {
47
+ this.update(clockSlot, head);
43
48
  return this.active;
44
49
  }
45
50
 
46
- update(clockSlot: Slot): void {
51
+ update(clockSlot: Slot, head: ProtoBlock): void {
47
52
  if (clockSlot <= this.lastUpdatedSlot) {
48
53
  return;
49
54
  }
50
55
  this.lastUpdatedSlot = clockSlot;
51
56
 
52
- // Exclude clockSlot itself, its payload reveal may still be in flight
53
- const {blocksPresent, payloadsRevealed} = this.modules.forkChoice.getPayloadRevealCounts(
57
+ // Exclude clockSlot itself, its payload status may still be unresolved
58
+ const {full, empty} = this.modules.forkChoice.getCanonicalPayloadCounts(
54
59
  Math.max(clockSlot - this.faultInspectionWindow, 0),
55
- clockSlot - 1
60
+ clockSlot - 1,
61
+ head
56
62
  );
57
- const faults = blocksPresent - payloadsRevealed;
63
+ const canonicalBlocks = full + empty;
58
64
 
59
65
  const wasActive = this.active;
60
- // Scale the fault budget by blocks present so sparse windows still trigger on high non-reveal rates
61
- this.active = faults * this.faultInspectionWindow > this.allowedFaults * blocksPresent;
66
+ // Scale the fault budget by canonical blocks so sparse windows still trigger on high EMPTY rates
67
+ const exceedsFaultBudget = empty * this.faultInspectionWindow > this.allowedFaults * canonicalBlocks;
68
+ if (exceedsFaultBudget) {
69
+ this.active = true;
70
+ } else if (canonicalBlocks >= MIN_BLOCKS_TO_DEACTIVATE) {
71
+ // Require a minimum sample within the fault budget before accepting builder bids again
72
+ this.active = false;
73
+ }
62
74
 
63
75
  this.modules.metrics?.builderCircuitBreaker.active.set(this.active ? 1 : 0);
64
- this.modules.metrics?.builderCircuitBreaker.faults.set(faults);
65
- this.modules.metrics?.builderCircuitBreaker.blocksPresent.set(blocksPresent);
66
- this.modules.metrics?.builderCircuitBreaker.payloadsRevealed.set(payloadsRevealed);
76
+ this.modules.metrics?.builderCircuitBreaker.canonicalBlocks.set(canonicalBlocks);
77
+ this.modules.metrics?.builderCircuitBreaker.fullBlocks.set(full);
78
+ this.modules.metrics?.builderCircuitBreaker.emptyBlocks.set(empty);
67
79
 
68
80
  const logCtx = {
69
- blocksPresent,
70
- faults,
81
+ clockSlot,
82
+ canonicalBlocks,
83
+ full,
84
+ empty,
71
85
  faultInspectionWindow: this.faultInspectionWindow,
72
86
  allowedFaults: this.allowedFaults,
73
87
  };
@@ -1072,6 +1072,7 @@ export class BeaconChain implements IBeaconChain {
1072
1072
  graffiti,
1073
1073
  slot,
1074
1074
  feeRecipient,
1075
+ strictFeeRecipientCheck,
1075
1076
  commonBlockBodyPromise,
1076
1077
  parentBlock,
1077
1078
  builderBid,
@@ -1100,6 +1101,7 @@ export class BeaconChain implements IBeaconChain {
1100
1101
  graffiti,
1101
1102
  slot,
1102
1103
  feeRecipient,
1104
+ strictFeeRecipientCheck,
1103
1105
  parentBlock,
1104
1106
  proposerIndex,
1105
1107
  proposerPubKey,
@@ -1217,7 +1219,12 @@ export class BeaconChain implements IBeaconChain {
1217
1219
  this.emitter.emit(routes.events.EventType.proposerSlashing, proposerSlashing);
1218
1220
  this.emitter.emit(ChainEvent.publishProposerSlashing, proposerSlashing);
1219
1221
  this.metrics?.opPool.proposerSlashingsProduced.inc();
1220
- this.logger.info("Produced proposer slashing from observed equivocation", {slot: blockSlot, proposerIndex});
1222
+ this.logger.info("Produced proposer slashing from observed equivocation", {
1223
+ slot: blockSlot,
1224
+ proposerIndex,
1225
+ header1Root: toRootHex(ssz.phase0.BeaconBlockHeader.hashTreeRoot(header1.message)),
1226
+ header2Root: toRootHex(ssz.phase0.BeaconBlockHeader.hashTreeRoot(header2.message)),
1227
+ });
1221
1228
  } finally {
1222
1229
  this.producingProposerSlashing.delete(proposerIndex);
1223
1230
  }
@@ -118,7 +118,7 @@ export type BlockErrorType =
118
118
  | {code: BlockErrorCode.GENESIS_BLOCK}
119
119
  | {code: BlockErrorCode.WOULD_REVERT_FINALIZED_SLOT; blockSlot: Slot; finalizedSlot: Slot}
120
120
  | {code: BlockErrorCode.ALREADY_KNOWN; root: RootHex}
121
- | {code: BlockErrorCode.REPEAT_PROPOSAL; proposerIndex: ValidatorIndex}
121
+ | {code: BlockErrorCode.REPEAT_PROPOSAL; proposerIndex: ValidatorIndex; root: RootHex}
122
122
  | {code: BlockErrorCode.BLOCK_SLOT_LIMIT_REACHED}
123
123
  | {code: BlockErrorCode.INCORRECT_PROPOSER; proposerIndex: ValidatorIndex}
124
124
  | {code: BlockErrorCode.PROPOSAL_SIGNATURE_INVALID; blockSlot: Slot}
@@ -53,7 +53,7 @@ export type IChainOptions = BlockProcessOpts &
53
53
  nativeStateView?: boolean;
54
54
  /** Builder circuit breaker fault inspection window in slots */
55
55
  faultInspectionWindow?: number;
56
- /** Allowed unrevealed payloads within the fault inspection window */
56
+ /** Canonical EMPTY blocks allowed per `faultInspectionWindow` observed blocks */
57
57
  allowedFaults?: number;
58
58
  };
59
59
 
@@ -86,8 +86,6 @@ export type BlockProcessOpts = {
86
86
  verifyOnly?: boolean;
87
87
  /** Used to specify to skip execution payload validation */
88
88
  skipVerifyExecutionPayload?: boolean;
89
- /** Used to specify to skip block signatures validation */
90
- skipVerifyBlockSignatures?: boolean;
91
89
  };
92
90
 
93
91
  export type PoolOpts = {
@@ -169,7 +169,7 @@ export class PrepareNextSlotScheduler {
169
169
  }
170
170
 
171
171
  if (isForkPostGloas(fork)) {
172
- this.chain.builderCircuitBreaker.update(clockSlot);
172
+ this.chain.builderCircuitBreaker.update(clockSlot, updatedHead);
173
173
  } else {
174
174
  // Update the builder status, if enabled shoot an api call to check status
175
175
  this.chain.updateBuilderStatus(clockSlot);
@@ -224,7 +224,7 @@ export class PrepareNextSlotScheduler {
224
224
  this.metrics?.blockPayload.payloadAdvancePrepTime.observe(preparationTime);
225
225
 
226
226
  const safeBlockHash = getSafeExecutionBlockHash(this.chain.forkChoice, this.logger);
227
- const finalizedBlockHash = getFinalizedExecutionBlockHash(this.chain.forkChoice, this.logger);
227
+ const finalizedBlockHash = getFinalizedExecutionBlockHash(this.chain.forkChoice);
228
228
 
229
229
  // awaiting here instead of throwing an async call because there is no other task
230
230
  // left for scheduler and this gives nice semantics to catch and log errors in the
@@ -100,6 +100,8 @@ export type BlockAttributes = {
100
100
  slot: Slot;
101
101
  parentBlock: ProtoBlock;
102
102
  feeRecipient?: string;
103
+ /** Verify that a locally produced execution payload uses `feeRecipient`. */
104
+ strictFeeRecipientCheck?: boolean;
103
105
  /** When provided, build block with this builder bid instead of a self-build bid */
104
106
  builderBid?: gloas.SignedExecutionPayloadBid;
105
107
  };
@@ -201,6 +203,7 @@ export async function produceBlockBody<T extends BlockType>(
201
203
  const {
202
204
  slot: blockSlot,
203
205
  feeRecipient: requestedFeeRecipient,
206
+ strictFeeRecipientCheck,
204
207
  parentBlock,
205
208
  proposerIndex,
206
209
  proposerPubKey,
@@ -271,7 +274,7 @@ export async function produceBlockBody<T extends BlockType>(
271
274
  // full and blinded no longer makes sense in gloas, it might be a good idea to move
272
275
  // this into a completely separate function and have pre/post gloas more separated
273
276
  const safeBlockHash = getSafeExecutionBlockHash(this.forkChoice, this.logger);
274
- const finalizedBlockHash = getFinalizedExecutionBlockHash(this.forkChoice, this.logger);
277
+ const finalizedBlockHash = getFinalizedExecutionBlockHash(this.forkChoice);
275
278
  // TODO GLOAS: post-Gloas, proposer feeRecipient is also carried (signed) in
276
279
  // ProposerPreferencesPool. Consider using this unified cache instead
277
280
  // see https://github.com/ChainSafe/lodestar/issues/9379
@@ -334,6 +337,16 @@ export async function produceBlockBody<T extends BlockType>(
334
337
  executionPayloadValue = payloadRes.executionPayloadValue;
335
338
  shouldOverrideBuilder = payloadRes.shouldOverrideBuilder;
336
339
 
340
+ if (
341
+ strictFeeRecipientCheck &&
342
+ requestedFeeRecipient &&
343
+ !byteArrayEquals(executionPayload.feeRecipient, fromHex(requestedFeeRecipient))
344
+ ) {
345
+ throw Error(
346
+ `Invalid feeRecipient set in engine payload expected=${requestedFeeRecipient} actual=${toHex(executionPayload.feeRecipient)}`
347
+ );
348
+ }
349
+
337
350
  if (blobsBundle === undefined) {
338
351
  throw Error(`Missing blobsBundle response from getPayload at fork=${fork}`);
339
352
  }
@@ -417,7 +430,7 @@ export async function produceBlockBody<T extends BlockType>(
417
430
  }
418
431
 
419
432
  const safeBlockHash = getSafeExecutionBlockHash(this.forkChoice, this.logger);
420
- const finalizedBlockHash = getFinalizedExecutionBlockHash(this.forkChoice, this.logger);
433
+ const finalizedBlockHash = getFinalizedExecutionBlockHash(this.forkChoice);
421
434
  const feeRecipient = requestedFeeRecipient ?? this.beaconProposerCache.getOrDefault(proposerIndex);
422
435
  const feeRecipientType = requestedFeeRecipient
423
436
  ? "requested"