@lodestar/beacon-node 1.46.0-dev.44e482c861 → 1.46.0-dev.47415586e0

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 (70) hide show
  1. package/lib/api/impl/beacon/blocks/index.d.ts.map +1 -1
  2. package/lib/api/impl/beacon/blocks/index.js +1 -1
  3. package/lib/api/impl/beacon/blocks/index.js.map +1 -1
  4. package/lib/chain/blocks/importBlock.d.ts.map +1 -1
  5. package/lib/chain/blocks/importBlock.js +3 -3
  6. package/lib/chain/blocks/importBlock.js.map +1 -1
  7. package/lib/chain/blocks/importExecutionPayload.d.ts.map +1 -1
  8. package/lib/chain/blocks/importExecutionPayload.js +3 -4
  9. package/lib/chain/blocks/importExecutionPayload.js.map +1 -1
  10. package/lib/chain/blocks/utils/chainSegment.d.ts.map +1 -1
  11. package/lib/chain/blocks/utils/chainSegment.js +31 -25
  12. package/lib/chain/blocks/utils/chainSegment.js.map +1 -1
  13. package/lib/chain/blocks/verifyBlock.d.ts.map +1 -1
  14. package/lib/chain/blocks/verifyBlock.js +11 -3
  15. package/lib/chain/blocks/verifyBlock.js.map +1 -1
  16. package/lib/chain/chain.d.ts +5 -0
  17. package/lib/chain/chain.d.ts.map +1 -1
  18. package/lib/chain/chain.js +55 -0
  19. package/lib/chain/chain.js.map +1 -1
  20. package/lib/chain/emitter.d.ts +6 -0
  21. package/lib/chain/emitter.d.ts.map +1 -1
  22. package/lib/chain/emitter.js +5 -0
  23. package/lib/chain/emitter.js.map +1 -1
  24. package/lib/chain/interface.d.ts +2 -0
  25. package/lib/chain/interface.d.ts.map +1 -1
  26. package/lib/chain/options.d.ts +2 -0
  27. package/lib/chain/options.d.ts.map +1 -1
  28. package/lib/chain/options.js +1 -0
  29. package/lib/chain/options.js.map +1 -1
  30. package/lib/chain/prepareNextSlot.d.ts.map +1 -1
  31. package/lib/chain/prepareNextSlot.js +74 -6
  32. package/lib/chain/prepareNextSlot.js.map +1 -1
  33. package/lib/chain/produceBlock/produceBlockBody.d.ts.map +1 -1
  34. package/lib/chain/produceBlock/produceBlockBody.js +6 -6
  35. package/lib/chain/produceBlock/produceBlockBody.js.map +1 -1
  36. package/lib/chain/seenCache/seenBlockProposers.d.ts +8 -3
  37. package/lib/chain/seenCache/seenBlockProposers.d.ts.map +1 -1
  38. package/lib/chain/seenCache/seenBlockProposers.js +25 -11
  39. package/lib/chain/seenCache/seenBlockProposers.js.map +1 -1
  40. package/lib/chain/validation/block.d.ts.map +1 -1
  41. package/lib/chain/validation/block.js +8 -5
  42. package/lib/chain/validation/block.js.map +1 -1
  43. package/lib/metrics/metrics/lodestar.d.ts +14 -0
  44. package/lib/metrics/metrics/lodestar.d.ts.map +1 -1
  45. package/lib/metrics/metrics/lodestar.js +38 -0
  46. package/lib/metrics/metrics/lodestar.js.map +1 -1
  47. package/lib/network/network.d.ts +1 -0
  48. package/lib/network/network.d.ts.map +1 -1
  49. package/lib/network/network.js +11 -1
  50. package/lib/network/network.js.map +1 -1
  51. package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
  52. package/lib/network/processor/gossipHandlers.js +8 -0
  53. package/lib/network/processor/gossipHandlers.js.map +1 -1
  54. package/package.json +14 -14
  55. package/src/api/impl/beacon/blocks/index.ts +6 -1
  56. package/src/chain/blocks/importBlock.ts +3 -2
  57. package/src/chain/blocks/importExecutionPayload.ts +8 -4
  58. package/src/chain/blocks/utils/chainSegment.ts +35 -28
  59. package/src/chain/blocks/verifyBlock.ts +16 -6
  60. package/src/chain/chain.ts +60 -0
  61. package/src/chain/emitter.ts +7 -0
  62. package/src/chain/interface.ts +3 -0
  63. package/src/chain/options.ts +3 -0
  64. package/src/chain/prepareNextSlot.ts +82 -6
  65. package/src/chain/produceBlock/produceBlockBody.ts +11 -6
  66. package/src/chain/seenCache/seenBlockProposers.ts +40 -14
  67. package/src/chain/validation/block.ts +8 -4
  68. package/src/metrics/metrics/lodestar.ts +39 -0
  69. package/src/network/network.ts +16 -1
  70. package/src/network/processor/gossipHandlers.ts +7 -0
@@ -1,6 +1,6 @@
1
1
  import {routes} from "@lodestar/api";
2
2
  import {ChainForkConfig} from "@lodestar/config";
3
- import {getSafeExecutionBlockHash} from "@lodestar/fork-choice";
3
+ import {getFinalizedExecutionBlockHash, getSafeExecutionBlockHash} from "@lodestar/fork-choice";
4
4
  import {
5
5
  ForkPostBellatrix,
6
6
  ForkSeq,
@@ -10,17 +10,20 @@ import {
10
10
  isForkPostGloas,
11
11
  } from "@lodestar/params";
12
12
  import {
13
+ GLOAS_PREVERIFY_WINDOW_EPOCHS,
13
14
  IBeaconStateView,
14
15
  IBeaconStateViewBellatrix,
16
+ MAX_BUILDER_DEPOSITS_PER_SLOT,
15
17
  StateHashTreeRootSource,
16
18
  computeEpochAtSlot,
17
19
  computeTimeAtSlot,
18
20
  isStatePostBellatrix,
21
+ isStatePostFulu,
19
22
  isStatePostGloas,
20
23
  } from "@lodestar/state-transition";
21
24
  import {Bytes32, Slot, ValidatorIndex} from "@lodestar/types";
22
25
  import {Logger, fromHex, isErrorAborted, sleep} from "@lodestar/utils";
23
- import {GENESIS_SLOT, ZERO_HASH_HEX} from "../constants/constants.js";
26
+ import {GENESIS_SLOT} from "../constants/constants.js";
24
27
  import {BuilderStatus} from "../execution/builder/http.js";
25
28
  import {Metrics} from "../metrics/index.js";
26
29
  import {ClockEvent} from "../util/clock.js";
@@ -37,6 +40,11 @@ export const PREPARE_NEXT_SLOT_BPS = 6667;
37
40
  /* We don't want to do more epoch transition than this */
38
41
  const PREPARE_EPOCH_LIMIT = 1;
39
42
 
43
+ /* Fraction of a slot (out of 10_000) the pre-Gloas builder-deposit pre-verify may spend per tick.
44
+ * 2000 = 20% of slot (2.4s on mainnet), within the spare window left after PREPARE_NEXT_SLOT_BPS.
45
+ * Expressed in bps so it scales with slot duration (mainnet vs minimal/devnet). */
46
+ const BUILDER_PREVERIFY_LIMIT_BPS = 2000;
47
+
40
48
  /**
41
49
  * At Bellatrix, if we are responsible for proposing in next slot, we want to prepare payload
42
50
  * 4s before the start of next slot at PREPARE_NEXT_SLOT_BPS of the current slot.
@@ -117,6 +125,7 @@ export class PrepareNextSlotScheduler {
117
125
  ? this.metrics?.precomputeNextEpochTransition.duration.startTimer()
118
126
  : null;
119
127
  const start = Date.now();
128
+ let feeRecipient: string | undefined;
120
129
  // No need to wait for this or the clock drift
121
130
  if (isForkPostBellatrix(fork)) {
122
131
  let preparedState: IBeaconStateView | undefined;
@@ -140,7 +149,7 @@ export class PrepareNextSlotScheduler {
140
149
  };
141
150
 
142
151
  const proposerIndex = await getProposerIndex();
143
- const feeRecipient = this.chain.beaconProposerCache.get(proposerIndex);
152
+ feeRecipient = this.chain.beaconProposerCache.get(proposerIndex);
144
153
 
145
154
  if (feeRecipient) {
146
155
  // If we are proposing next slot, we need to predict if we can proposer-boost-reorg or not
@@ -214,9 +223,8 @@ export class PrepareNextSlotScheduler {
214
223
  computeTimeAtSlot(this.config, prepareSlot, this.chain.genesisTime) - Date.now() / 1000;
215
224
  this.metrics?.blockPayload.payloadAdvancePrepTime.observe(preparationTime);
216
225
 
217
- const safeBlockHash = getSafeExecutionBlockHash(this.chain.forkChoice);
218
- const finalizedBlockHash =
219
- this.chain.forkChoice.getFinalizedBlock().executionPayloadBlockHash ?? ZERO_HASH_HEX;
226
+ const safeBlockHash = getSafeExecutionBlockHash(this.chain.forkChoice, this.logger);
227
+ const finalizedBlockHash = getFinalizedExecutionBlockHash(this.chain.forkChoice, this.logger);
220
228
 
221
229
  // awaiting here instead of throwing an async call because there is no other task
222
230
  // left for scheduler and this gives nice semantics to catch and log errors in the
@@ -287,6 +295,74 @@ export class PrepareNextSlotScheduler {
287
295
  });
288
296
 
289
297
  precomputeEpochTransitionTimer?.();
298
+
299
+ // Nothing more to do on an epoch-transition slot: that precompute already ran a full (>1s)
300
+ // epoch transition, so we don't pile the builder-deposit pre-verify (below) on top of it.
301
+ return;
302
+ }
303
+
304
+ // Pre-verify builder-deposit signatures in the GLOAS_PREVERIFY_WINDOW_EPOCHS epochs leading
305
+ // up to the Gloas fork, off the block-import hot path, so onboardBuildersFromPendingDeposits
306
+ // at the transition is mostly cache hits. Reached only on non-epoch-transition slots (the
307
+ // epoch-transition branch returned above). Runs on the head state — its pendingDeposits are
308
+ // what the fork transition derives from, and its epochCtx holds the shared, app-wide
309
+ // builderDepositSignatureCache.
310
+ const headState = this.chain.getHeadState();
311
+ if (isStatePostFulu(headState)) {
312
+ const gloasEpoch = this.config.GLOAS_FORK_EPOCH;
313
+ const finalizedEpoch = this.chain.forkChoice.getFinalizedCheckpoint().epoch;
314
+ if (finalizedEpoch >= gloasEpoch) {
315
+ // Gloas is finalized — the pre-verify cache has served its purpose; release its memory.
316
+ headState.clearPreGloasBuilderDepositCache();
317
+ } else {
318
+ const clockEpoch = computeEpochAtSlot(clockSlot);
319
+ if (
320
+ ForkSeq[fork] < ForkSeq.gloas && // only before the fork
321
+ clockEpoch >= gloasEpoch - GLOAS_PREVERIFY_WINDOW_EPOCHS && // Infinity when Gloas unscheduled → skip
322
+ clockEpoch < gloasEpoch &&
323
+ // skip when we're the next-slot proposer — don't compete with block-production prep.
324
+ feeRecipient === undefined
325
+ ) {
326
+ const maxDurationMs = this.config.getSlotComponentDurationMs(BUILDER_PREVERIFY_LIMIT_BPS);
327
+ const preVerifyTimer = this.metrics?.builderDepositPreVerify.duration.startTimer();
328
+ const result = headState.preVerifyBuilderDepositsPreGloas(MAX_BUILDER_DEPOSITS_PER_SLOT, maxDurationMs);
329
+ preVerifyTimer?.();
330
+
331
+ const preVerifyMetrics = this.metrics?.builderDepositPreVerify;
332
+ if (preVerifyMetrics) {
333
+ preVerifyMetrics.pendingDeposits.set(result.pendingDepositsCount);
334
+ preVerifyMetrics.cachedDeposits.set(result.totalCachedDeposits);
335
+ preVerifyMetrics.scannedDeposits.set(result.scannedPendingDeposits);
336
+ preVerifyMetrics.builderPubkeys.set(result.totalBuilderPubkeys);
337
+ if (result.validBuilderSignaturesCount > 0) {
338
+ preVerifyMetrics.validSignatures.inc({type: "builder"}, result.validBuilderSignaturesCount);
339
+ }
340
+ if (result.validValidatorSignaturesCount > 0) {
341
+ preVerifyMetrics.validSignatures.inc({type: "validator"}, result.validValidatorSignaturesCount);
342
+ }
343
+ if (result.invalidBuilderSignaturesCount > 0) {
344
+ preVerifyMetrics.invalidSignatures.inc({type: "builder"}, result.invalidBuilderSignaturesCount);
345
+ }
346
+ if (result.invalidValidatorSignaturesCount > 0) {
347
+ preVerifyMetrics.invalidSignatures.inc({type: "validator"}, result.invalidValidatorSignaturesCount);
348
+ }
349
+ }
350
+
351
+ // Always log (once per slot, only within the pre-fork window) so devnets show the
352
+ // pre-verify draining even on all-cache-hit ticks.
353
+ this.logger.verbose("PrepareNextSlotScheduler pre-verified deposit signatures", {
354
+ clockSlot,
355
+ validBuilderSignaturesCount: result.validBuilderSignaturesCount,
356
+ invalidBuilderSignaturesCount: result.invalidBuilderSignaturesCount,
357
+ validValidatorSignaturesCount: result.validValidatorSignaturesCount,
358
+ invalidValidatorSignaturesCount: result.invalidValidatorSignaturesCount,
359
+ scannedPendingDeposits: result.scannedPendingDeposits,
360
+ totalCachedDeposits: result.totalCachedDeposits,
361
+ totalBuilderPubkeys: result.totalBuilderPubkeys,
362
+ pendingDepositsCount: result.pendingDepositsCount,
363
+ });
364
+ }
365
+ }
290
366
  }
291
367
  } catch (e) {
292
368
  if (!isErrorAborted(e) && !isQueueErrorAborted(e)) {
@@ -1,6 +1,11 @@
1
1
  import {BitArray} from "@chainsafe/ssz";
2
2
  import {ChainForkConfig} from "@lodestar/config";
3
- import {IForkChoice, ProtoBlock, getSafeExecutionBlockHash} from "@lodestar/fork-choice";
3
+ import {
4
+ IForkChoice,
5
+ ProtoBlock,
6
+ getFinalizedExecutionBlockHash,
7
+ getSafeExecutionBlockHash,
8
+ } from "@lodestar/fork-choice";
4
9
  import {
5
10
  BUILDER_INDEX_SELF_BUILD,
6
11
  ForkName,
@@ -265,8 +270,8 @@ export async function produceBlockBody<T extends BlockType>(
265
270
  // TODO GLOAS: support non self-building here, the block type differentiation between
266
271
  // full and blinded no longer makes sense in gloas, it might be a good idea to move
267
272
  // this into a completely separate function and have pre/post gloas more separated
268
- const safeBlockHash = getSafeExecutionBlockHash(this.forkChoice);
269
- const finalizedBlockHash = this.forkChoice.getFinalizedBlock().executionPayloadBlockHash ?? ZERO_HASH_HEX;
273
+ const safeBlockHash = getSafeExecutionBlockHash(this.forkChoice, this.logger);
274
+ const finalizedBlockHash = getFinalizedExecutionBlockHash(this.forkChoice, this.logger);
270
275
  // TODO GLOAS: post-Gloas, proposer feeRecipient is also carried (signed) in
271
276
  // ProposerPreferencesPool. Consider using this unified cache instead
272
277
  // see https://github.com/ChainSafe/lodestar/issues/9379
@@ -411,8 +416,8 @@ export async function produceBlockBody<T extends BlockType>(
411
416
  throw new Error("Expected Bellatrix state for execution block production");
412
417
  }
413
418
 
414
- const safeBlockHash = getSafeExecutionBlockHash(this.forkChoice);
415
- const finalizedBlockHash = this.forkChoice.getFinalizedBlock().executionPayloadBlockHash ?? ZERO_HASH_HEX;
419
+ const safeBlockHash = getSafeExecutionBlockHash(this.forkChoice, this.logger);
420
+ const finalizedBlockHash = getFinalizedExecutionBlockHash(this.forkChoice, this.logger);
416
421
  const feeRecipient = requestedFeeRecipient ?? this.beaconProposerCache.getOrDefault(proposerIndex);
417
422
  const feeRecipientType = requestedFeeRecipient
418
423
  ? "requested"
@@ -1037,7 +1042,7 @@ export async function produceCommonBlockBody<T extends BlockType>(
1037
1042
  graffiti,
1038
1043
  // Eth1 data voting is no longer required since electra
1039
1044
  eth1Data: currentState.eth1Data,
1040
- proposerSlashings,
1045
+ proposerSlashings: this.opts.disableProposerSlashings === true ? [] : proposerSlashings,
1041
1046
  attesterSlashings,
1042
1047
  attestations,
1043
1048
  // Since electra, deposits are processed by the execution layer,
@@ -1,5 +1,5 @@
1
1
  import {computeStartSlotAtEpoch} from "@lodestar/state-transition";
2
- import {Epoch, RootHex, Slot, ValidatorIndex} from "@lodestar/types";
2
+ import {Epoch, RootHex, Slot, ValidatorIndex, phase0} from "@lodestar/types";
3
3
  import {MapDef} from "@lodestar/utils";
4
4
 
5
5
  /** Two distinct block roots signed by the same proposer for the same slot are sufficient to establish an equivocation */
@@ -13,13 +13,17 @@ const MAX_BLOCK_ROOTS_PER_PROPOSAL = 2;
13
13
  * equivocation evidence, but the block may still fail other validation. Such a block must not mark the proposal as
14
14
  * known, since that would cause a later valid block for the same slot and proposer to be ignored as a repeat proposal.
15
15
  *
16
+ * The signed block header of each observed root is kept so a proposer slashing can be produced from the two
17
+ * conflicting headers once an equivocation is established.
18
+ *
16
19
  * The cache is pruned on finalization and bounds the number of roots stored per proposer and slot
17
20
  */
18
21
  export class SeenBlockProposers {
19
22
  private readonly proposerIndexesBySlot = new MapDef<Slot, Set<ValidatorIndex>>(() => new Set<ValidatorIndex>());
20
- private readonly blockRootsBySlot = new MapDef<Slot, MapDef<ValidatorIndex, Set<RootHex>>>(
21
- () => new MapDef<ValidatorIndex, Set<RootHex>>(() => new Set<RootHex>())
22
- );
23
+ private readonly signedBlockHeadersBySlot = new MapDef<
24
+ Slot,
25
+ MapDef<ValidatorIndex, Map<RootHex, phase0.SignedBeaconBlockHeader>>
26
+ >(() => new MapDef<ValidatorIndex, Map<RootHex, phase0.SignedBeaconBlockHeader>>(() => new Map()));
23
27
  private finalizedSlot: Slot = 0;
24
28
 
25
29
  isKnown(blockSlot: Slot, proposerIndex: ValidatorIndex): boolean {
@@ -27,27 +31,49 @@ export class SeenBlockProposers {
27
31
  }
28
32
 
29
33
  hasBlockRoot(blockSlot: Slot, proposerIndex: ValidatorIndex, blockRoot: RootHex): boolean {
30
- return this.blockRootsBySlot.get(blockSlot)?.get(proposerIndex)?.has(blockRoot) === true;
34
+ return this.signedBlockHeadersBySlot.get(blockSlot)?.get(proposerIndex)?.has(blockRoot) === true;
31
35
  }
32
36
 
33
37
  isEquivocating(blockSlot: Slot, proposerIndex: ValidatorIndex): boolean {
34
- return (this.blockRootsBySlot.get(blockSlot)?.get(proposerIndex)?.size ?? 0) >= MAX_BLOCK_ROOTS_PER_PROPOSAL;
38
+ return (
39
+ (this.signedBlockHeadersBySlot.get(blockSlot)?.get(proposerIndex)?.size ?? 0) >= MAX_BLOCK_ROOTS_PER_PROPOSAL
40
+ );
35
41
  }
36
42
 
37
43
  getConflictingBlockRoots(blockSlot: Slot, proposerIndex: ValidatorIndex, blockRoot: RootHex): RootHex[] {
38
- const roots = this.blockRootsBySlot.get(blockSlot)?.get(proposerIndex);
39
- return roots === undefined ? [] : Array.from(roots).filter((root) => root !== blockRoot);
44
+ const signedBlockHeaderByRoot = this.signedBlockHeadersBySlot.get(blockSlot)?.get(proposerIndex);
45
+ return signedBlockHeaderByRoot === undefined
46
+ ? []
47
+ : Array.from(signedBlockHeaderByRoot.keys()).filter((root) => root !== blockRoot);
48
+ }
49
+
50
+ /** Return the two signed block headers that establish an equivocation, or null if there is none */
51
+ getEquivocationHeaders(
52
+ blockSlot: Slot,
53
+ proposerIndex: ValidatorIndex
54
+ ): [phase0.SignedBeaconBlockHeader, phase0.SignedBeaconBlockHeader] | null {
55
+ const signedBlockHeaderByRoot = this.signedBlockHeadersBySlot.get(blockSlot)?.get(proposerIndex);
56
+ if (signedBlockHeaderByRoot === undefined || signedBlockHeaderByRoot.size < MAX_BLOCK_ROOTS_PER_PROPOSAL) {
57
+ return null;
58
+ }
59
+ const [signedHeader1, signedHeader2] = signedBlockHeaderByRoot.values();
60
+ return [signedHeader1, signedHeader2];
40
61
  }
41
62
 
42
63
  /** Record a block only after its proposer signature has been verified */
43
- observeBlockRoot(blockSlot: Slot, proposerIndex: ValidatorIndex, blockRoot: RootHex): void {
64
+ observeBlockRoot(
65
+ blockSlot: Slot,
66
+ proposerIndex: ValidatorIndex,
67
+ blockRoot: RootHex,
68
+ signedBlockHeader: phase0.SignedBeaconBlockHeader
69
+ ): void {
44
70
  if (blockSlot < this.finalizedSlot) {
45
71
  throw Error(`blockSlot ${blockSlot} < finalizedSlot ${this.finalizedSlot}`);
46
72
  }
47
73
 
48
- const blockRoots = this.blockRootsBySlot.getOrDefault(blockSlot).getOrDefault(proposerIndex);
49
- if (blockRoots.size < MAX_BLOCK_ROOTS_PER_PROPOSAL) {
50
- blockRoots.add(blockRoot);
74
+ const signedBlockHeaderByRoot = this.signedBlockHeadersBySlot.getOrDefault(blockSlot).getOrDefault(proposerIndex);
75
+ if (signedBlockHeaderByRoot.size < MAX_BLOCK_ROOTS_PER_PROPOSAL && !signedBlockHeaderByRoot.has(blockRoot)) {
76
+ signedBlockHeaderByRoot.set(blockRoot, signedBlockHeader);
51
77
  }
52
78
  }
53
79
 
@@ -67,9 +93,9 @@ export class SeenBlockProposers {
67
93
  this.proposerIndexesBySlot.delete(slot);
68
94
  }
69
95
  }
70
- for (const slot of this.blockRootsBySlot.keys()) {
96
+ for (const slot of this.signedBlockHeadersBySlot.keys()) {
71
97
  if (slot < finalizedSlot) {
72
- this.blockRootsBySlot.delete(slot);
98
+ this.signedBlockHeadersBySlot.delete(slot);
73
99
  }
74
100
  }
75
101
  }
@@ -23,8 +23,9 @@ import {
23
23
  getBlockProposerSignatureSet,
24
24
  isExecutionBlockBodyType,
25
25
  isStatePostBellatrix,
26
+ signedBlockToSignedHeader,
26
27
  } from "@lodestar/state-transition";
27
- import {RootHex, SignedBeaconBlock, deneb, gloas, isGloasBeaconBlock} from "@lodestar/types";
28
+ import {RootHex, SignedBeaconBlock, deneb, gloas, isGloasBeaconBlock, ssz} from "@lodestar/types";
28
29
  import {byteArrayEquals, sleep, toRootHex} from "@lodestar/utils";
29
30
  import {BlockErrorCode, BlockGossipError, GossipAction} from "../errors/index.js";
30
31
  import {IBeaconChain} from "../interface.js";
@@ -75,7 +76,10 @@ export async function validateGossipBlock(
75
76
  // reboot if the `observed_block_producers` cache is empty. In that case, without this
76
77
  // check, we will load the parent and state from disk only to find out later that we
77
78
  // already know this block.
78
- const blockRoot = toRootHex(config.getForkTypes(blockSlot).BeaconBlock.hashTreeRoot(block));
79
+ // A block's hash tree root is identical to its header's, so the root is derived from the header
80
+ // which is also used as potential equivocation evidence
81
+ const signedBlockHeader = signedBlockToSignedHeader(config, signedBlock);
82
+ const blockRoot = toRootHex(ssz.phase0.BeaconBlockHeader.hashTreeRoot(signedBlockHeader.message));
79
83
  if (chain.forkChoice.getBlockHexDefaultStatus(blockRoot) !== null) {
80
84
  throw new BlockGossipError(GossipAction.IGNORE, {code: BlockErrorCode.ALREADY_KNOWN, root: blockRoot});
81
85
  }
@@ -89,7 +93,7 @@ export async function validateGossipBlock(
89
93
  if (chain.seenBlockProposers.isKnown(blockSlot, proposerIndex)) {
90
94
  if (!hasBlockRoot && !chain.seenBlockProposers.isEquivocating(blockSlot, proposerIndex)) {
91
95
  await verifyBlockProposerSignature(chain, signedBlock, blockRoot, {verifyOnMainThread: false});
92
- chain.seenBlockProposers.observeBlockRoot(blockSlot, proposerIndex, blockRoot);
96
+ chain.seenBlockProposers.observeBlockRoot(blockSlot, proposerIndex, blockRoot, signedBlockHeader);
93
97
  }
94
98
  throw new BlockGossipError(GossipAction.IGNORE, {code: BlockErrorCode.REPEAT_PROPOSAL, proposerIndex});
95
99
  }
@@ -276,7 +280,7 @@ export async function validateGossipBlock(
276
280
 
277
281
  // [REJECT] The proposer signature, signed_beacon_block.signature, is valid with respect to the proposer_index pubkey.
278
282
  await verifyBlockProposerSignature(chain, signedBlock, blockRoot);
279
- chain.seenBlockProposers.observeBlockRoot(blockSlot, proposerIndex, blockRoot);
283
+ chain.seenBlockProposers.observeBlockRoot(blockSlot, proposerIndex, blockRoot, signedBlockHeader);
280
284
 
281
285
  // [REJECT] The block is proposed by the expected proposer_index for the block's slot in the context of the current
282
286
  // shuffling (defined by parent_root/slot). If the proposer_index cannot immediately be verified against the expected
@@ -1179,6 +1179,10 @@ export function createLodestarMetrics(
1179
1179
  name: "lodestar_oppool_proposer_slashing_pool_size",
1180
1180
  help: "Current size of the ProposerSlashingPool",
1181
1181
  }),
1182
+ proposerSlashingsProduced: register.counter({
1183
+ name: "lodestar_oppool_proposer_slashings_produced_total",
1184
+ help: "Total number of proposer slashings produced from observed equivocations",
1185
+ }),
1182
1186
  voluntaryExitPoolSize: register.gauge({
1183
1187
  name: "lodestar_oppool_voluntary_exit_pool_size",
1184
1188
  help: "Current size of the VoluntaryExitPool",
@@ -1708,6 +1712,41 @@ export function createLodestarMetrics(
1708
1712
  }),
1709
1713
  },
1710
1714
 
1715
+ // Pre-verify builder-deposit signatures in the epochs before the Gloas fork (prepareNextSlot).
1716
+ builderDepositPreVerify: {
1717
+ duration: register.histogram({
1718
+ name: "lodestar_builder_deposit_preverify_duration_seconds",
1719
+ help: "Duration of one preVerifyBuilderDepositsPreGloas call (BLS batch verify + scan)",
1720
+ buckets: [1, 2, 3, 4],
1721
+ }),
1722
+ pendingDeposits: register.gauge({
1723
+ name: "lodestar_builder_deposit_preverify_pending_deposits",
1724
+ help: "Current state.pendingDeposits length (backlog to drain before the fork)",
1725
+ }),
1726
+ cachedDeposits: register.gauge({
1727
+ name: "lodestar_builder_deposit_preverify_cached_deposits",
1728
+ help: "Deposit signatures verified & cached so far this pre-fork window (valid + invalid)",
1729
+ }),
1730
+ scannedDeposits: register.gauge({
1731
+ name: "lodestar_builder_deposit_preverify_scanned_deposits",
1732
+ help: "Pending deposits examined by the last pre-verify call (< pending_deposits ⇒ cap hit)",
1733
+ }),
1734
+ builderPubkeys: register.gauge({
1735
+ name: "lodestar_builder_deposit_preverify_builder_pubkeys",
1736
+ help: "Distinct builder pubkeys tracked this window (validator deposits for these are pre-verified)",
1737
+ }),
1738
+ validSignatures: register.counter<{type: "builder" | "validator"}>({
1739
+ name: "lodestar_builder_deposit_preverify_valid_signatures_total",
1740
+ help: "Cumulative deposit signatures whose BLS verification passed (VALID), by credential type",
1741
+ labelNames: ["type"],
1742
+ }),
1743
+ invalidSignatures: register.counter<{type: "builder" | "validator"}>({
1744
+ name: "lodestar_builder_deposit_preverify_invalid_signatures_total",
1745
+ help: "Cumulative deposit signatures whose BLS verification failed (INVALID), by credential type",
1746
+ labelNames: ["type"],
1747
+ }),
1748
+ },
1749
+
1711
1750
  // reprocess attestations
1712
1751
  reprocessApiAttestations: {
1713
1752
  total: register.gauge({
@@ -147,6 +147,7 @@ export class Network implements INetwork {
147
147
  this.chain.emitter.on(ChainEvent.updateTargetCustodyGroupCount, this.onTargetGroupCountUpdated);
148
148
  this.chain.emitter.on(ChainEvent.publishDataColumns, this.onPublishDataColumns);
149
149
  this.chain.emitter.on(ChainEvent.publishBlobSidecars, this.onPublishBlobSidecars);
150
+ this.chain.emitter.on(ChainEvent.publishProposerSlashing, this.onPublishProposerSlashing);
150
151
  this.chain.emitter.on(ChainEvent.updateStatus, this.onUpdateStatus);
151
152
  }
152
153
 
@@ -244,6 +245,7 @@ export class Network implements INetwork {
244
245
  this.chain.emitter.off(ChainEvent.updateTargetCustodyGroupCount, this.onTargetGroupCountUpdated);
245
246
  this.chain.emitter.off(ChainEvent.publishDataColumns, this.onPublishDataColumns);
246
247
  this.chain.emitter.off(ChainEvent.publishBlobSidecars, this.onPublishBlobSidecars);
248
+ this.chain.emitter.off(ChainEvent.publishProposerSlashing, this.onPublishProposerSlashing);
247
249
  this.chain.emitter.off(ChainEvent.updateStatus, this.onUpdateStatus);
248
250
  await this.core.close();
249
251
 
@@ -455,7 +457,8 @@ export class Network implements INetwork {
455
457
 
456
458
  return this.publishGossip<GossipType.proposer_slashing>(
457
459
  {type: GossipType.proposer_slashing, boundary},
458
- proposerSlashing
460
+ proposerSlashing,
461
+ {ignoreDuplicatePublishError: true}
459
462
  );
460
463
  }
461
464
 
@@ -877,6 +880,18 @@ export class Network implements INetwork {
877
880
  return promiseAllMaybeAsync(sidecars.map((sidecar) => () => this.publishBlobSidecar(sidecar)));
878
881
  };
879
882
 
883
+ private onPublishProposerSlashing = async (proposerSlashing: phase0.ProposerSlashing): Promise<void> => {
884
+ try {
885
+ await this.publishProposerSlashing(proposerSlashing);
886
+ } catch (e) {
887
+ this.logger.debug(
888
+ "Error publishing proposer slashing",
889
+ {proposerIndex: proposerSlashing.signedHeader1.message.proposerIndex},
890
+ e as Error
891
+ );
892
+ }
893
+ };
894
+
880
895
  private onUpdateStatus = async (): Promise<void> => {
881
896
  await this.core.updateStatus(this.chain.getStatus());
882
897
  };
@@ -257,6 +257,13 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
257
257
  chain.seenPayloadEnvelopeInputCache.prune(blockRootHex);
258
258
  }
259
259
  throw e;
260
+ } finally {
261
+ // The block received from the network may have established an equivocation, either by conflicting
262
+ // with a previously observed block root (REPEAT_PROPOSAL) or with a root observed during validation
263
+ const proposerIndex = signedBlock.message.proposerIndex;
264
+ if (chain.seenBlockProposers.isEquivocating(slot, proposerIndex)) {
265
+ chain.processProposerEquivocation(slot, proposerIndex);
266
+ }
260
267
  }
261
268
  }
262
269