@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
@@ -3,7 +3,7 @@ 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 */
6
- const MAX_BLOCK_ROOTS_PER_PROPOSAL = 2;
6
+ const MIN_EQUIVOCATION_BLOCK_ROOTS_PER_PROPOSAL = 2;
7
7
 
8
8
  /**
9
9
  * Keeps a cache to filter block proposals from the same validator in the same slot.
@@ -19,7 +19,9 @@ const MAX_BLOCK_ROOTS_PER_PROPOSAL = 2;
19
19
  * The cache is pruned on finalization and bounds the number of roots stored per proposer and slot
20
20
  */
21
21
  export class SeenBlockProposers {
22
- private readonly proposerIndexesBySlot = new MapDef<Slot, Set<ValidatorIndex>>(() => new Set<ValidatorIndex>());
22
+ private readonly proposerIndexesBySlot = new MapDef<Slot, Map<ValidatorIndex, RootHex>>(
23
+ () => new Map<ValidatorIndex, RootHex>()
24
+ );
23
25
  private readonly signedBlockHeadersBySlot = new MapDef<
24
26
  Slot,
25
27
  MapDef<ValidatorIndex, Map<RootHex, phase0.SignedBeaconBlockHeader>>
@@ -30,13 +32,22 @@ export class SeenBlockProposers {
30
32
  return this.proposerIndexesBySlot.get(blockSlot)?.has(proposerIndex) === true;
31
33
  }
32
34
 
35
+ /**
36
+ * The block proposer is known at slot with a different root.
37
+ */
38
+ isRepeatProposal(blockSlot: Slot, proposerIndex: ValidatorIndex, blockRoot: RootHex): boolean {
39
+ const knownRoot = this.proposerIndexesBySlot.get(blockSlot)?.get(proposerIndex);
40
+ return knownRoot !== undefined && knownRoot !== blockRoot;
41
+ }
42
+
33
43
  hasBlockRoot(blockSlot: Slot, proposerIndex: ValidatorIndex, blockRoot: RootHex): boolean {
34
44
  return this.signedBlockHeadersBySlot.get(blockSlot)?.get(proposerIndex)?.has(blockRoot) === true;
35
45
  }
36
46
 
37
47
  isEquivocating(blockSlot: Slot, proposerIndex: ValidatorIndex): boolean {
38
48
  return (
39
- (this.signedBlockHeadersBySlot.get(blockSlot)?.get(proposerIndex)?.size ?? 0) >= MAX_BLOCK_ROOTS_PER_PROPOSAL
49
+ (this.signedBlockHeadersBySlot.get(blockSlot)?.get(proposerIndex)?.size ?? 0) >=
50
+ MIN_EQUIVOCATION_BLOCK_ROOTS_PER_PROPOSAL
40
51
  );
41
52
  }
42
53
 
@@ -53,7 +64,10 @@ export class SeenBlockProposers {
53
64
  proposerIndex: ValidatorIndex
54
65
  ): [phase0.SignedBeaconBlockHeader, phase0.SignedBeaconBlockHeader] | null {
55
66
  const signedBlockHeaderByRoot = this.signedBlockHeadersBySlot.get(blockSlot)?.get(proposerIndex);
56
- if (signedBlockHeaderByRoot === undefined || signedBlockHeaderByRoot.size < MAX_BLOCK_ROOTS_PER_PROPOSAL) {
67
+ if (
68
+ signedBlockHeaderByRoot === undefined ||
69
+ signedBlockHeaderByRoot.size < MIN_EQUIVOCATION_BLOCK_ROOTS_PER_PROPOSAL
70
+ ) {
57
71
  return null;
58
72
  }
59
73
  const [signedHeader1, signedHeader2] = signedBlockHeaderByRoot.values();
@@ -72,18 +86,21 @@ export class SeenBlockProposers {
72
86
  }
73
87
 
74
88
  const signedBlockHeaderByRoot = this.signedBlockHeadersBySlot.getOrDefault(blockSlot).getOrDefault(proposerIndex);
75
- if (signedBlockHeaderByRoot.size < MAX_BLOCK_ROOTS_PER_PROPOSAL && !signedBlockHeaderByRoot.has(blockRoot)) {
89
+ if (
90
+ signedBlockHeaderByRoot.size < MIN_EQUIVOCATION_BLOCK_ROOTS_PER_PROPOSAL &&
91
+ !signedBlockHeaderByRoot.has(blockRoot)
92
+ ) {
76
93
  signedBlockHeaderByRoot.set(blockRoot, signedBlockHeader);
77
94
  }
78
95
  }
79
96
 
80
97
  /** Mark a block as known from gossip or another block import path */
81
- add(blockSlot: Slot, proposerIndex: ValidatorIndex): void {
98
+ add(blockSlot: Slot, proposerIndex: ValidatorIndex, blockRoot: RootHex): void {
82
99
  if (blockSlot < this.finalizedSlot) {
83
100
  throw Error(`blockSlot ${blockSlot} < finalizedSlot ${this.finalizedSlot}`);
84
101
  }
85
102
 
86
- this.proposerIndexesBySlot.getOrDefault(blockSlot).add(proposerIndex);
103
+ this.proposerIndexesBySlot.getOrDefault(blockSlot).set(proposerIndex, blockRoot);
87
104
  }
88
105
 
89
106
  prune(finalizedSlot: Slot): void {
@@ -89,13 +89,20 @@ export async function validateGossipBlock(
89
89
 
90
90
  // [IGNORE] The block is the first block with valid signature received for the proposer for the slot, signed_beacon_block.message.slot.
91
91
  const proposerIndex = block.proposerIndex;
92
- const hasBlockRoot = chain.seenBlockProposers.hasBlockRoot(blockSlot, proposerIndex, blockRoot);
93
92
  if (chain.seenBlockProposers.isKnown(blockSlot, proposerIndex)) {
94
- if (!hasBlockRoot && !chain.seenBlockProposers.isEquivocating(blockSlot, proposerIndex)) {
95
- await verifyBlockProposerSignature(chain, signedBlock, blockRoot, {verifyOnMainThread: false});
96
- chain.seenBlockProposers.observeBlockRoot(blockSlot, proposerIndex, blockRoot, signedBlockHeader);
93
+ if (chain.seenBlockProposers.isRepeatProposal(blockSlot, proposerIndex, blockRoot)) {
94
+ const hasBlockRoot = chain.seenBlockProposers.hasBlockRoot(blockSlot, proposerIndex, blockRoot);
95
+ if (!hasBlockRoot && !chain.seenBlockProposers.isEquivocating(blockSlot, proposerIndex)) {
96
+ await verifyBlockProposerSignature(chain, signedBlock, blockRoot, {verifyOnMainThread: false});
97
+ chain.seenBlockProposers.observeBlockRoot(blockSlot, proposerIndex, blockRoot, signedBlockHeader);
98
+ }
99
+ throw new BlockGossipError(GossipAction.IGNORE, {
100
+ code: BlockErrorCode.REPEAT_PROPOSAL,
101
+ proposerIndex,
102
+ root: blockRoot,
103
+ });
97
104
  }
98
- throw new BlockGossipError(GossipAction.IGNORE, {code: BlockErrorCode.REPEAT_PROPOSAL, proposerIndex});
105
+ throw new BlockGossipError(GossipAction.IGNORE, {code: BlockErrorCode.ALREADY_KNOWN, root: blockRoot});
99
106
  }
100
107
 
101
108
  // [REJECT] The current finalized_checkpoint is an ancestor of block -- i.e.
@@ -301,10 +308,17 @@ export async function validateGossipBlock(
301
308
 
302
309
  // Check again after all async validation and the early-block delay so concurrent proposals cannot both pass
303
310
  if (chain.seenBlockProposers.isKnown(blockSlot, proposerIndex)) {
304
- throw new BlockGossipError(GossipAction.IGNORE, {code: BlockErrorCode.REPEAT_PROPOSAL, proposerIndex});
311
+ if (chain.seenBlockProposers.isRepeatProposal(blockSlot, proposerIndex, blockRoot)) {
312
+ throw new BlockGossipError(GossipAction.IGNORE, {
313
+ code: BlockErrorCode.REPEAT_PROPOSAL,
314
+ proposerIndex,
315
+ root: blockRoot,
316
+ });
317
+ }
318
+ throw new BlockGossipError(GossipAction.IGNORE, {code: BlockErrorCode.ALREADY_KNOWN, root: blockRoot});
305
319
  }
306
320
 
307
- chain.seenBlockProposers.add(blockSlot, proposerIndex);
321
+ chain.seenBlockProposers.add(blockSlot, proposerIndex, blockRoot);
308
322
 
309
323
  return {skippedSlots};
310
324
  }
@@ -71,15 +71,18 @@ export class NoBidReceived extends Error {
71
71
  }
72
72
 
73
73
  /**
74
- * Beacon clients select randomized values from the following ranges when initializing
75
- * the circuit breaker (so at boot time and once for each unique boot).
74
+ * Default circuit breaker parameters:
76
75
  *
77
- * ALLOWED_FAULTS: between 1 and SLOTS_PER_EPOCH // 4
78
- * FAULT_INSPECTION_WINDOW: between SLOTS_PER_EPOCH and 2 * SLOTS_PER_EPOCH
76
+ * SLOTS_PER_EPOCH <= FAULT_INSPECTION_WINDOW < 2 * SLOTS_PER_EPOCH (randomized at initialization)
77
+ * ALLOWED_FAULTS: FAULT_INSPECTION_WINDOW // 4
79
78
  *
80
- * The values are randomized per node so builders cannot predict when a given proposer will
81
- * fall back to local blocks. With fixed thresholds a builder could withhold payloads right up
82
- * to the limit without ever tripping the breaker.
79
+ * e.g. on mainnet SLOTS_PER_EPOCH is 32, so FAULT_INSPECTION_WINDOW is between 32 and 63,
80
+ * and a window of 40 results in ALLOWED_FAULTS = 10.
81
+ *
82
+ * The randomized default window keeps the exact slots under inspection unpredictable per node,
83
+ * so a builder cannot tell when past faults age out of a given proposer's window and time
84
+ * withholding around trip or recovery points. Explicitly configured windows are clamped to at
85
+ * least SLOTS_PER_EPOCH, and configured allowedFaults is capped at the default ~25% budget.
83
86
  */
84
87
  export function getFaultInspectionParams(opts: {faultInspectionWindow?: number; allowedFaults?: number}): {
85
88
  faultInspectionWindow: number;
@@ -90,10 +93,7 @@ export function getFaultInspectionParams(opts: {faultInspectionWindow?: number;
90
93
  SLOTS_PER_EPOCH
91
94
  );
92
95
  // allowedFaults should be < faultInspectionWindow, limiting them to faultInspectionWindow/4
93
- const allowedFaults = Math.min(
94
- opts.allowedFaults ?? Math.floor(faultInspectionWindow / 4),
95
- Math.floor(faultInspectionWindow / 4)
96
- );
96
+ const allowedFaults = Math.min(opts.allowedFaults ?? Infinity, Math.floor(faultInspectionWindow / 4));
97
97
  return {faultInspectionWindow, allowedFaults};
98
98
  }
99
99
 
@@ -143,17 +143,17 @@ export function createBeaconMetrics(register: RegistryMetricCreator) {
143
143
  name: "beacon_builder_circuit_breaker_active",
144
144
  help: "Whether the builder circuit breaker is active (1) causing builder bids to be ignored",
145
145
  }),
146
- faults: register.gauge({
147
- name: "beacon_builder_circuit_breaker_faults",
148
- help: "Count of blocks with unrevealed payloads in the fault inspection window",
146
+ canonicalBlocks: register.gauge({
147
+ name: "beacon_builder_circuit_breaker_canonical_blocks",
148
+ help: "Count of canonical FULL and EMPTY blocks in the fault inspection window",
149
149
  }),
150
- blocksPresent: register.gauge({
151
- name: "beacon_builder_circuit_breaker_blocks_present",
152
- help: "Count of blocks present in the fault inspection window",
150
+ fullBlocks: register.gauge({
151
+ name: "beacon_builder_circuit_breaker_full_blocks",
152
+ help: "Count of canonical FULL blocks in the fault inspection window",
153
153
  }),
154
- payloadsRevealed: register.gauge({
155
- name: "beacon_builder_circuit_breaker_payloads_revealed",
156
- help: "Count of blocks with revealed payloads in the fault inspection window",
154
+ emptyBlocks: register.gauge({
155
+ name: "beacon_builder_circuit_breaker_empty_blocks",
156
+ help: "Count of canonical EMPTY blocks in the fault inspection window",
157
157
  }),
158
158
  },
159
159
 
@@ -908,6 +908,22 @@ export function createLodestarMetrics(
908
908
  labelNames: ["error"],
909
909
  }),
910
910
  },
911
+ gossipExecutionPayloadBid: {
912
+ elapsedTimeTillReceived: register.histogram<{source: OpSource}>({
913
+ name: "lodestar_gossip_execution_payload_bid_elapsed_time_till_received_seconds",
914
+ help: "Time elapsed between the bid's slot start and the time the execution payload bid was received (negative = received before its slot, broadcast in the previous slot; positive = received during its own slot, late)",
915
+ labelNames: ["source"],
916
+ buckets: [-6, -3, -1, 0, 3],
917
+ }),
918
+ },
919
+ gossipPayloadAttestationMessage: {
920
+ elapsedTimeTillReceived: register.histogram<{source: OpSource}>({
921
+ name: "lodestar_gossip_payload_attestation_message_elapsed_time_till_received_seconds",
922
+ help: "Time elapsed between slot time and the time the payload attestation message was received",
923
+ labelNames: ["source"],
924
+ buckets: [3, 6, 9, 12],
925
+ }),
926
+ },
911
927
  // recovery in the case of specific blob rows required
912
928
  recoverBlobSidecars: {
913
929
  blobsReconstructed: register.counter({
@@ -713,9 +713,32 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
713
713
  const {serializedData} = gossipData;
714
714
 
715
715
  const signedBlock = sszDeserialize(topic, serializedData);
716
- const blockInput = await validateBeaconBlock(signedBlock, topic.boundary.fork, peerIdStr, seenTimestampSec);
717
- chain.serializedCache.set(signedBlock, serializedData);
718
- handleValidBeaconBlock(blockInput, peerIdStr, seenTimestampSec);
716
+ try {
717
+ const blockInput = await validateBeaconBlock(signedBlock, topic.boundary.fork, peerIdStr, seenTimestampSec);
718
+ chain.serializedCache.set(signedBlock, serializedData);
719
+ handleValidBeaconBlock(blockInput, peerIdStr, seenTimestampSec);
720
+ } catch (e) {
721
+ // Spec: IGNORE the block, ie not to re-publish to peers
722
+ // but we should still import an equivocating (REPEAT_PROPOSAL) block into fork choice because we don't
723
+ // know if this block (or the 1st known block with same slot) will become canonical yet
724
+ if (
725
+ e instanceof BlockGossipError &&
726
+ e.type.code === BlockErrorCode.REPEAT_PROPOSAL &&
727
+ // this is make sure the block's proposer signature was verified, it should be true anyway
728
+ chain.seenBlockProposers.hasBlockRoot(signedBlock.message.slot, e.type.proposerIndex, e.type.root)
729
+ ) {
730
+ // blockInput was optimistically seeded in validateBeaconBlock and retained on IGNORE
731
+ const blockInput = chain.seenBlockInputCache.get(e.type.root);
732
+ if (blockInput) {
733
+ chain.serializedCache.set(signedBlock, serializedData);
734
+ // this is technically not a valid gossip block but gossip validation is a cheap subset of checks
735
+ // this runs the full state transition, so importing an equivocating-but-valid block here is safe.
736
+ handleValidBeaconBlock(blockInput, peerIdStr, seenTimestampSec);
737
+ }
738
+ }
739
+ // rethrow so gossipValidatorFn maps IGNORE -> TopicValidatorResult.Ignore (message not forwarded)
740
+ throw e;
741
+ }
719
742
  },
720
743
 
721
744
  [GossipType.blob_sidecar]: async ({
@@ -1232,11 +1255,15 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
1232
1255
  [GossipType.payload_attestation_message]: async ({
1233
1256
  gossipData,
1234
1257
  topic,
1258
+ seenTimestampSec,
1235
1259
  }: GossipHandlerParamGeneric<GossipType.payload_attestation_message>) => {
1236
1260
  const {serializedData} = gossipData;
1237
1261
  const payloadAttestationMessage = sszDeserialize(topic, serializedData);
1238
1262
  const validationResult = await validateGossipPayloadAttestationMessage(chain, payloadAttestationMessage);
1239
1263
 
1264
+ const delaySec = chain.clock.secFromSlot(payloadAttestationMessage.data.slot, seenTimestampSec);
1265
+ metrics?.gossipPayloadAttestationMessage.elapsedTimeTillReceived.observe({source: OpSource.gossip}, delaySec);
1266
+
1240
1267
  try {
1241
1268
  const insertOutcome = chain.payloadAttestationPool.add(
1242
1269
  payloadAttestationMessage,
@@ -1263,11 +1290,16 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
1263
1290
  [GossipType.execution_payload_bid]: async ({
1264
1291
  gossipData,
1265
1292
  topic,
1293
+ seenTimestampSec,
1266
1294
  }: GossipHandlerParamGeneric<GossipType.execution_payload_bid>) => {
1267
1295
  const {serializedData} = gossipData;
1268
1296
  const executionPayloadBid = sszDeserialize(topic, serializedData);
1269
1297
  const {proposerIndex} = await validateGossipExecutionPayloadBid(chain, executionPayloadBid);
1270
1298
 
1299
+ // this could be negative, because it's most likely the bid of next slot comes at this clock slot
1300
+ const elapsedSec = chain.clock.secFromSlot(executionPayloadBid.message.slot, seenTimestampSec);
1301
+ metrics?.gossipExecutionPayloadBid.elapsedTimeTillReceived.observe({source: OpSource.gossip}, elapsedSec);
1302
+
1271
1303
  // Handle valid payload bid by storing in a bid pool
1272
1304
  try {
1273
1305
  const insertOutcome = chain.executionPayloadBidPool.add(executionPayloadBid);
@@ -593,7 +593,7 @@ export class Batch {
593
593
  if (allComplete) {
594
594
  const attempt: Attempt = {
595
595
  peers: this.getSuccessfulPeers(),
596
- hash: hashBlocks(blocks, this.config),
596
+ hash: hashBlocks(blocks, payloadEnvelopes),
597
597
  };
598
598
  this.state = {status: BatchStatus.AwaitingProcessing, blocks, payloadEnvelopes: newPayloadEnvelopes, attempt};
599
599
  } else {
@@ -1,27 +1,62 @@
1
- import {ChainForkConfig} from "@lodestar/config";
2
- import {RootHex, SignedBeaconBlock} from "@lodestar/types";
3
- import {toRootHex} from "@lodestar/utils";
1
+ import {digest} from "@chainsafe/as-sha256";
2
+ import {RootHex, Slot, ssz} from "@lodestar/types";
3
+ import {fromHex, toRootHex} from "@lodestar/utils";
4
4
  import {IBlockInput} from "../../../chain/blocks/blockInput/types.js";
5
+ import {PayloadEnvelopeInput} from "../../../chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.js";
6
+
7
+ const ROOT_SIZE = 32;
8
+ const SIGNATURE_SIZE = 96;
9
+ /** Every block and every payload envelope contributes a fixed-size (root, signature) entry */
10
+ const ENTRY_SIZE = ROOT_SIZE + SIGNATURE_SIZE;
11
+ /** uint32 LE counts of block and envelope entries, so the variable-length sections are self-describing */
12
+ const COUNT_SIZE = 4;
13
+ const HEADER_SIZE = COUNT_SIZE * 2;
5
14
 
6
15
  /**
7
- * String to uniquely identify block segments. Used for peer scoring and to compare if batches are equivalent.
16
+ * Root uniquely identifying a batch attempt (its blocks AND payloads). Used for peer scoring and to
17
+ * compare if two attempts are equivalent.
18
+ *
19
+ * Signatures are part of the id for BOTH blocks and payload envelopes, not just their message roots:
20
+ * a peer serving a correct message with a garbage signature would otherwise produce the same id as
21
+ * the honest attempt and escape scoring in `SyncChain.advanceChain`. Signatures are not verified at
22
+ * download time, only during processChainSegment (block signatures in verifyBlocksSignatures,
23
+ * envelope signatures in importExecutionPayload).
24
+ *
25
+ * Entries are written as raw bytes into a single exactly-sized buffer and digested once, so an
26
+ * `Attempt` retains 32 bytes rather than the ~16 KB a full 32-slot gloas batch would need if the
27
+ * roots and signatures were concatenated as hex. sha256 (not a 64-bit hash) because a peer that
28
+ * could force a collision with the winning attempt would escape peer scoring.
8
29
  */
9
- export function hashBlocks(blocks: IBlockInput[], config: ChainForkConfig): RootHex {
10
- switch (blocks.length) {
11
- case 0:
12
- return "0x";
13
- case 1: {
14
- const block0 = blocks[0].getBlock();
15
- return toRootHex(config.getForkTypes(block0.message.slot).SignedBeaconBlock.hashTreeRoot(block0));
16
- }
17
- default: {
18
- const block0 = blocks[0].getBlock();
19
- const blockN = blocks.at(-1)?.getBlock() as SignedBeaconBlock;
20
- return (
21
- // TODO(fulu): should we be doing checks for presence to make sure these do not blow up?
22
- toRootHex(config.getForkTypes(block0.message.slot).SignedBeaconBlock.hashTreeRoot(block0)) +
23
- toRootHex(config.getForkTypes(blockN.message.slot).SignedBeaconBlock.hashTreeRoot(blockN))
24
- );
25
- }
30
+ export function hashBlocks(blocks: IBlockInput[], payloadEnvelopes: Map<Slot, PayloadEnvelopeInput> | null): RootHex {
31
+ // Envelopes without a payload carry no attributable data, so they are skipped entirely. A `null`
32
+ // map and a map whose entries all lack a payload are therefore equivalent, which is intended.
33
+ const envelopes =
34
+ payloadEnvelopes && payloadEnvelopes.size > 0
35
+ ? Array.from(payloadEnvelopes.entries())
36
+ .filter(([, envelope]) => envelope.hasPayloadEnvelope())
37
+ .sort(([slotA], [slotB]) => slotA - slotB)
38
+ : [];
39
+
40
+ const buf = Buffer.allocUnsafe(HEADER_SIZE + (blocks.length + envelopes.length) * ENTRY_SIZE);
41
+ buf.writeUInt32LE(blocks.length, 0);
42
+ buf.writeUInt32LE(envelopes.length, COUNT_SIZE);
43
+ let offset = HEADER_SIZE;
44
+
45
+ for (const block of blocks) {
46
+ buf.set(fromHex(block.blockRootHex), offset);
47
+ offset += ROOT_SIZE;
48
+ buf.set(block.getBlock().signature, offset);
49
+ offset += SIGNATURE_SIZE;
26
50
  }
51
+
52
+ for (const [, envelope] of envelopes) {
53
+ const signedEnvelope = envelope.getPayloadEnvelope();
54
+ // envelope's root is cached thanks to cachePermanentRootStruct, so this avoids re-hashing
55
+ buf.set(ssz.gloas.ExecutionPayloadEnvelope.hashTreeRoot(signedEnvelope.message), offset);
56
+ offset += ROOT_SIZE;
57
+ buf.set(signedEnvelope.signature, offset);
58
+ offset += SIGNATURE_SIZE;
59
+ }
60
+
61
+ return toRootHex(digest(buf));
27
62
  }
@@ -1,6 +1,6 @@
1
1
  import {routes} from "@lodestar/api";
2
2
  import {ChainForkConfig} from "@lodestar/config";
3
- import {ForkSeq} from "@lodestar/params";
3
+ import {ForkSeq, isForkPostGloas} from "@lodestar/params";
4
4
  import {RequestError, RequestErrorCode} from "@lodestar/reqresp";
5
5
  import {computeTimeAtSlot} from "@lodestar/state-transition";
6
6
  import {RootHex, Slot, gloas} from "@lodestar/types";
@@ -764,9 +764,10 @@ export class BlockInputSync {
764
764
  // this prevents unbundling attack
765
765
  // see https://lighthouse-blog.sigmaprime.io/mev-unbundling-rpc.html
766
766
  const {slot: blockSlot, proposerIndex} = pendingBlock.blockInput.getBlock().message;
767
+ const blockRootHex = pendingBlock.blockInput.blockRootHex;
767
768
  const logCtx = {
768
769
  slot: blockSlot,
769
- root: pendingBlock.blockInput.blockRootHex,
770
+ root: blockRootHex,
770
771
  delaySec: this.chain.clock.secFromSlot(blockSlot),
771
772
  };
772
773
  this.logger.verbose("Processing downloaded block", logCtx);
@@ -780,7 +781,7 @@ export class BlockInputSync {
780
781
  // eligible for proposer boost to prevent unbundling attack
781
782
  this.logger.verbose("Avoid proposer boost for this block of known proposer", {
782
783
  slot: blockSlot,
783
- root: pendingBlock.blockInput.blockRootHex,
784
+ root: blockRootHex,
784
785
  proposerIndex,
785
786
  });
786
787
  await sleep(proposerBoostWindowMs);
@@ -807,12 +808,24 @@ export class BlockInputSync {
807
808
  if (!res.err) {
808
809
  this.logger.verbose("Processed block from unknown sync", logCtx);
809
810
  // no need to update status to "processed", delete anyway
810
- this.pendingBlocks.delete(pendingBlock.blockInput.blockRootHex);
811
+ this.pendingBlocks.delete(blockRootHex);
812
+
813
+ if (isForkPostGloas(fork)) {
814
+ const payloadInput = this.chain.seenPayloadEnvelopeInputCache.get(blockRootHex);
815
+ if (!payloadInput) {
816
+ this.logger.warn("PayloadEnvelopeInput not seeded during unknown sync processReadyBlock()", logCtx);
817
+ } else {
818
+ // Similar to the gossip path: immediately attempt fetch of data columns from the execution engine.
819
+ // The bid already carries the kzg commitments, so there is no reason to wait for the payload to arrive.
820
+ this.chain.getBlobsTracker.triggerGetBlobs(payloadInput);
821
+ }
822
+ }
823
+
811
824
  // Re-enter the scheduler so descendants blocked on either parent blocks or parent payloads
812
825
  // are advanced through the same dependency checks as every other pending item.
813
826
  this.triggerUnknownBlockSearch();
814
827
  } else {
815
- const errorData = {slot: pendingBlock.blockInput.slot, root: pendingBlock.blockInput.blockRootHex};
828
+ const errorData = {slot: pendingBlock.blockInput.slot, root: blockRootHex};
816
829
  if (res.err instanceof BlockError) {
817
830
  switch (res.err.type.code) {
818
831
  // This cases are already handled with `{ignoreIfKnown: true}`