@lodestar/beacon-node 1.47.0-rc.0 → 1.48.0-dev.7d85330f92

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 (86) hide show
  1. package/lib/api/impl/beacon/blocks/index.d.ts.map +1 -1
  2. package/lib/api/impl/beacon/blocks/index.js +4 -0
  3. package/lib/api/impl/beacon/blocks/index.js.map +1 -1
  4. package/lib/api/impl/validator/index.d.ts.map +1 -1
  5. package/lib/api/impl/validator/index.js +5 -0
  6. package/lib/api/impl/validator/index.js.map +1 -1
  7. package/lib/chain/blocks/index.js +7 -7
  8. package/lib/chain/blocks/index.js.map +1 -1
  9. package/lib/chain/blocks/verifyBlock.d.ts.map +1 -1
  10. package/lib/chain/blocks/verifyBlock.js +5 -2
  11. package/lib/chain/blocks/verifyBlock.js.map +1 -1
  12. package/lib/chain/chain.d.ts +1 -1
  13. package/lib/chain/chain.d.ts.map +1 -1
  14. package/lib/chain/chain.js +2 -1
  15. package/lib/chain/chain.js.map +1 -1
  16. package/lib/chain/errors/executionPayloadBid.d.ts +5 -0
  17. package/lib/chain/errors/executionPayloadBid.d.ts.map +1 -1
  18. package/lib/chain/errors/executionPayloadBid.js +1 -0
  19. package/lib/chain/errors/executionPayloadBid.js.map +1 -1
  20. package/lib/chain/interface.d.ts +2 -2
  21. package/lib/chain/interface.d.ts.map +1 -1
  22. package/lib/chain/prepareNextSlot.d.ts +5 -0
  23. package/lib/chain/prepareNextSlot.d.ts.map +1 -1
  24. package/lib/chain/prepareNextSlot.js +24 -1
  25. package/lib/chain/prepareNextSlot.js.map +1 -1
  26. package/lib/chain/produceBlock/computeNewStateRoot.d.ts.map +1 -1
  27. package/lib/chain/produceBlock/computeNewStateRoot.js +3 -25
  28. package/lib/chain/produceBlock/computeNewStateRoot.js.map +1 -1
  29. package/lib/chain/regen/interface.d.ts +1 -0
  30. package/lib/chain/regen/interface.d.ts.map +1 -1
  31. package/lib/chain/regen/interface.js +1 -0
  32. package/lib/chain/regen/interface.js.map +1 -1
  33. package/lib/chain/validation/block.d.ts.map +1 -1
  34. package/lib/chain/validation/block.js +40 -10
  35. package/lib/chain/validation/block.js.map +1 -1
  36. package/lib/chain/validation/executionPayloadBid.d.ts +15 -3
  37. package/lib/chain/validation/executionPayloadBid.d.ts.map +1 -1
  38. package/lib/chain/validation/executionPayloadBid.js +163 -8
  39. package/lib/chain/validation/executionPayloadBid.js.map +1 -1
  40. package/lib/execution/builder/apiClient.d.ts +6 -0
  41. package/lib/execution/builder/apiClient.d.ts.map +1 -1
  42. package/lib/execution/builder/apiClient.js +19 -1
  43. package/lib/execution/builder/apiClient.js.map +1 -1
  44. package/lib/execution/builder/validateBid.d.ts +1 -0
  45. package/lib/execution/builder/validateBid.d.ts.map +1 -1
  46. package/lib/execution/builder/validateBid.js +9 -1
  47. package/lib/execution/builder/validateBid.js.map +1 -1
  48. package/lib/metrics/metrics/lodestar.d.ts +10 -1
  49. package/lib/metrics/metrics/lodestar.d.ts.map +1 -1
  50. package/lib/metrics/metrics/lodestar.js +19 -3
  51. package/lib/metrics/metrics/lodestar.js.map +1 -1
  52. package/lib/network/network.d.ts.map +1 -1
  53. package/lib/network/network.js +5 -4
  54. package/lib/network/network.js.map +1 -1
  55. package/lib/network/processor/gossipQueues/indexed.d.ts.map +1 -1
  56. package/lib/network/processor/gossipQueues/indexed.js +0 -1
  57. package/lib/network/processor/gossipQueues/indexed.js.map +1 -1
  58. package/lib/network/processor/index.d.ts.map +1 -1
  59. package/lib/network/processor/index.js +16 -8
  60. package/lib/network/processor/index.js.map +1 -1
  61. package/lib/sync/range/chain.d.ts.map +1 -1
  62. package/lib/sync/range/chain.js +9 -2
  63. package/lib/sync/range/chain.js.map +1 -1
  64. package/lib/sync/range/range.js +1 -1
  65. package/lib/sync/range/range.js.map +1 -1
  66. package/package.json +15 -15
  67. package/src/api/impl/beacon/blocks/index.ts +4 -0
  68. package/src/api/impl/validator/index.ts +5 -0
  69. package/src/chain/blocks/index.ts +7 -7
  70. package/src/chain/blocks/verifyBlock.ts +5 -2
  71. package/src/chain/chain.ts +2 -1
  72. package/src/chain/errors/executionPayloadBid.ts +6 -0
  73. package/src/chain/interface.ts +2 -2
  74. package/src/chain/prepareNextSlot.ts +26 -1
  75. package/src/chain/produceBlock/computeNewStateRoot.ts +3 -38
  76. package/src/chain/regen/interface.ts +1 -0
  77. package/src/chain/validation/block.ts +43 -10
  78. package/src/chain/validation/executionPayloadBid.ts +190 -10
  79. package/src/execution/builder/apiClient.ts +21 -1
  80. package/src/execution/builder/validateBid.ts +16 -1
  81. package/src/metrics/metrics/lodestar.ts +21 -4
  82. package/src/network/network.ts +7 -4
  83. package/src/network/processor/gossipQueues/indexed.ts +0 -1
  84. package/src/network/processor/index.ts +25 -11
  85. package/src/sync/range/chain.ts +9 -2
  86. package/src/sync/range/range.ts +1 -1
@@ -1,4 +1,4 @@
1
- import {IForkChoice, ProtoBlock} from "@lodestar/fork-choice";
1
+ import {ProtoBlock} from "@lodestar/fork-choice";
2
2
  import {PAYLOAD_BUILDER_VERSION} from "@lodestar/params";
3
3
  import {
4
4
  computeEpochAtSlot,
@@ -46,11 +46,12 @@ function getMinBidValue(currentHighestBid: number): number {
46
46
  /**
47
47
  * Check whether a bid builds on one of the paths compatible with the local head branch.
48
48
  *
49
- * Building directly on the parent is allowed for proposer-boost reorgs outside epoch boundaries.
49
+ * Building directly on the parent is allowed for proposer-boost reorgs, including at epoch
50
+ * boundaries when the head is weak enough that a reorg onto the strong parent is predicted.
50
51
  * Otherwise the bid must build on the local head's full or empty payload variant, as selected for its slot.
51
52
  */
52
53
  function isBidCompatibleWithHead(
53
- forkChoice: IForkChoice,
54
+ chain: IBeaconChain,
54
55
  head: ProtoBlock,
55
56
  bidSlot: Slot,
56
57
  bidParentBlockRoot: RootHex,
@@ -60,9 +61,13 @@ function isBidCompatibleWithHead(
60
61
  const buildsOnParentPayload = bidParentBlockHash === head.parentBlockHash;
61
62
 
62
63
  if (buildsOnParentBlock && buildsOnParentPayload) {
63
- // The spec allows this at epoch boundaries, but Lodestar does not propagate these bids because validating
64
- // them requires an epoch transition for a parent state that cannot be used for proposer-boost reorgs.
65
- return !isStartSlotOfEpoch(bidSlot);
64
+ if (!isStartSlotOfEpoch(bidSlot)) {
65
+ return true;
66
+ }
67
+ // Epoch-boundary reorgs are allowed, but only propagate the bid if its parent matches the block
68
+ // we predict a proposer would reorg onto — predictProposerHead returns the strong parent only
69
+ // when the head is weak.
70
+ return chain.predictProposerHead().blockRoot === bidParentBlockRoot;
66
71
  }
67
72
 
68
73
  if (bidParentBlockRoot !== head.blockRoot) {
@@ -71,18 +76,170 @@ function isBidCompatibleWithHead(
71
76
 
72
77
  const buildsOnHeadPayload = bidParentBlockHash === head.executionPayloadBlockHash;
73
78
 
74
- if (forkChoice.shouldBuildOnFull(head, bidSlot)) {
79
+ if (chain.forkChoice.shouldBuildOnFull(head, bidSlot)) {
75
80
  return buildsOnHeadPayload;
76
81
  }
77
82
 
78
83
  return buildsOnParentPayload;
79
84
  }
80
85
 
86
+ /**
87
+ * Validation for bids submitted via the API by the operator's own builder.
88
+ *
89
+ * Only non-transient checks are applied: slot later than parent, zero execution payment, blob
90
+ * commitment count, builder eligibility, version and balance coverage, prev_randao, and signature.
91
+ * Transient gossip rules (head compatibility, first bid per tuple, value increment, proposer
92
+ * preferences) are not applied, since those only limit forwarding of peers' messages and the
93
+ * builder may legitimately bid on a branch this node does not consider head.
94
+ * The parent-payload builder-exit rule is omitted because this endpoint only accepts bids from
95
+ * the operator's own builder.
96
+ *
97
+ * Throws on any failed check. Also throws if the bid's parent block is unknown or its state is
98
+ * unavailable, since the checks cannot be evaluated against the parent branch.
99
+ */
81
100
  export async function validateApiExecutionPayloadBid(
82
101
  chain: IBeaconChain,
83
102
  signedExecutionPayloadBid: gloas.SignedExecutionPayloadBid
84
- ): Promise<{proposerIndex: ValidatorIndex}> {
85
- return validateExecutionPayloadBid(chain, signedExecutionPayloadBid);
103
+ ): Promise<void> {
104
+ const bid = signedExecutionPayloadBid.message;
105
+ const bidParentBlockRoot = toRootHex(bid.parentBlockRoot);
106
+
107
+ // [IGNORE] `bid.slot` is the current slot, or the next slot (`bid.slot - 1` is current), allowing for `MAXIMUM_GOSSIP_CLOCK_DISPARITY`.
108
+ if (
109
+ !chain.clock.isCurrentSlotGivenGossipDisparity(bid.slot) &&
110
+ !chain.clock.isCurrentSlotGivenGossipDisparity(bid.slot - 1)
111
+ ) {
112
+ throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
113
+ code: ExecutionPayloadBidErrorCode.INVALID_SLOT,
114
+ builderIndex: bid.builderIndex,
115
+ slot: bid.slot,
116
+ });
117
+ }
118
+
119
+ // [IGNORE] `bid.parent_block_root` is the hash tree root of a known beacon block in fork choice.
120
+ // Moved earlier than the spec ordering so we can derive the proposer dependent root for the
121
+ // proposer-preferences lookup below from a known fork-choice block.
122
+ const parentBlock = chain.forkChoice.getBlockHexDefaultStatus(bidParentBlockRoot);
123
+ if (parentBlock === null) {
124
+ throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
125
+ code: ExecutionPayloadBidErrorCode.UNKNOWN_BLOCK_ROOT,
126
+ parentBlockRoot: bidParentBlockRoot,
127
+ });
128
+ }
129
+
130
+ // [REJECT] The bid is for a higher slot than its parent block -- i.e.
131
+ // validate that `bid.slot` is greater than the slot of the block with root
132
+ // `bid.parent_block_root`.
133
+ if (bid.slot <= parentBlock.slot) {
134
+ throw new ExecutionPayloadBidError(GossipAction.REJECT, {
135
+ code: ExecutionPayloadBidErrorCode.NOT_LATER_THAN_PARENT,
136
+ parentSlot: parentBlock.slot,
137
+ slot: bid.slot,
138
+ });
139
+ }
140
+
141
+ // [REJECT] `bid.execution_payment` is zero.
142
+ if (bid.executionPayment !== 0n) {
143
+ throw new ExecutionPayloadBidError(GossipAction.REJECT, {
144
+ code: ExecutionPayloadBidErrorCode.NON_ZERO_EXECUTION_PAYMENT,
145
+ builderIndex: bid.builderIndex,
146
+ executionPayment: bid.executionPayment,
147
+ });
148
+ }
149
+
150
+ // [REJECT] The length of KZG commitments is less than or equal to the limitation defined in the
151
+ // consensus layer -- i.e. validate that
152
+ // `len(bid.blob_kzg_commitments) <= get_blob_parameters(compute_epoch_at_slot(bid.slot)).max_blobs_per_block`.
153
+ const blobKzgCommitmentsLen = bid.blobKzgCommitments.length;
154
+ const maxBlobsPerBlock = chain.config.getMaxBlobsPerBlock(computeEpochAtSlot(bid.slot));
155
+ if (blobKzgCommitmentsLen > maxBlobsPerBlock) {
156
+ throw new ExecutionPayloadBidError(GossipAction.REJECT, {
157
+ code: ExecutionPayloadBidErrorCode.TOO_MANY_KZG_COMMITMENTS,
158
+ blobKzgCommitmentsLen,
159
+ commitmentLimit: maxBlobsPerBlock,
160
+ });
161
+ }
162
+
163
+ const state = await chain.regen
164
+ .getBlockSlotState(parentBlock, bid.slot, {dontTransferCache: true}, RegenCaller.validateApiExecutionPayloadBid)
165
+ .catch(() => {
166
+ throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
167
+ code: ExecutionPayloadBidErrorCode.UNKNOWN_BLOCK_ROOT,
168
+ parentBlockRoot: bidParentBlockRoot,
169
+ });
170
+ });
171
+
172
+ if (!isStatePostGloas(state)) {
173
+ throw new Error(`Expected gloas+ state for execution payload bid validation, got fork=${state.forkName}`);
174
+ }
175
+
176
+ // [REJECT] `bid.builder_index` is within bounds -- i.e. `bid.builder_index < len(state.builders)`.
177
+ // `state.getBuilder` returns a lazy SSZ `getReadonly` view that is not bounds-checked eagerly; an
178
+ // out-of-range index only throws (`LeafNode has no right node`) on deferred field access (e.g. inside
179
+ // `isActiveBuilder`), escaping a try/catch around `getBuilder`. Check the length explicitly instead.
180
+ if (bid.builderIndex >= state.getBuildersLength()) {
181
+ throw new ExecutionPayloadBidError(GossipAction.REJECT, {
182
+ code: ExecutionPayloadBidErrorCode.BUILDER_NOT_ELIGIBLE,
183
+ builderIndex: bid.builderIndex,
184
+ });
185
+ }
186
+
187
+ // [REJECT] `bid.builder_index` is a valid/active builder index -- i.e.
188
+ // `is_active_builder(state, bid.builder_index)` returns `True`.
189
+ const builder = state.getBuilder(bid.builderIndex);
190
+ if (!isActiveBuilder(builder, state.finalizedCheckpoint.epoch)) {
191
+ throw new ExecutionPayloadBidError(GossipAction.REJECT, {
192
+ code: ExecutionPayloadBidErrorCode.BUILDER_NOT_ELIGIBLE,
193
+ builderIndex: bid.builderIndex,
194
+ });
195
+ }
196
+
197
+ // [REJECT] The builder version is `PAYLOAD_BUILDER_VERSION` -- i.e.
198
+ // `state.builders[bid.builder_index].version == PAYLOAD_BUILDER_VERSION`.
199
+ if (builder.version !== PAYLOAD_BUILDER_VERSION) {
200
+ throw new ExecutionPayloadBidError(GossipAction.REJECT, {
201
+ code: ExecutionPayloadBidErrorCode.INVALID_BUILDER_VERSION,
202
+ builderIndex: bid.builderIndex,
203
+ version: builder.version,
204
+ expectedVersion: PAYLOAD_BUILDER_VERSION,
205
+ });
206
+ }
207
+
208
+ // [IGNORE] `bid.value` is less or equal than the builder's excess balance --
209
+ // i.e. `can_builder_cover_bid(state, builder_index, amount)` returns `True`.
210
+ if (!state.canBuilderCoverBid(bid.builderIndex, bid.value)) {
211
+ throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
212
+ code: ExecutionPayloadBidErrorCode.BID_TOO_HIGH,
213
+ bidValue: bid.value,
214
+ builderBalance: builder.balance,
215
+ });
216
+ }
217
+
218
+ // [REJECT] `bid.prev_randao` is the correct RANDAO mix -- i.e. validate that
219
+ // `bid.prev_randao == get_randao_mix(parent_state, get_current_epoch(parent_state))`.
220
+ const randaoMix = state.getRandaoMix(computeEpochAtSlot(state.slot));
221
+ if (!byteArrayEquals(bid.prevRandao, randaoMix)) {
222
+ throw new ExecutionPayloadBidError(GossipAction.REJECT, {
223
+ code: ExecutionPayloadBidErrorCode.INVALID_PREV_RANDAO,
224
+ builderIndex: bid.builderIndex,
225
+ bidPrevRandao: toHex(bid.prevRandao),
226
+ expectedPrevRandao: toHex(randaoMix),
227
+ });
228
+ }
229
+
230
+ // [REJECT] `signed_execution_payload_bid.signature` is valid with respect to the `bid.builder_index`.
231
+ const signatureSet = createSingleSignatureSetFromComponents(
232
+ builder.pubkey,
233
+ getExecutionPayloadBidSigningRoot(chain.config, bid),
234
+ signedExecutionPayloadBid.signature
235
+ );
236
+ if (!(await chain.bls.verifySignatureSets([signatureSet]))) {
237
+ throw new ExecutionPayloadBidError(GossipAction.REJECT, {
238
+ code: ExecutionPayloadBidErrorCode.INVALID_SIGNATURE,
239
+ builderIndex: bid.builderIndex,
240
+ slot: bid.slot,
241
+ });
242
+ }
86
243
  }
87
244
 
88
245
  export async function validateGossipExecutionPayloadBid(
@@ -114,7 +271,7 @@ async function validateExecutionPayloadBid(
114
271
 
115
272
  // [IGNORE] The bid is compatible with the current head branch.
116
273
  const head = chain.forkChoice.getHead();
117
- if (!isBidCompatibleWithHead(chain.forkChoice, head, bid.slot, bidParentBlockRoot, bidParentBlockHash)) {
274
+ if (!isBidCompatibleWithHead(chain, head, bid.slot, bidParentBlockRoot, bidParentBlockHash)) {
118
275
  throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
119
276
  code: ExecutionPayloadBidErrorCode.INCOMPATIBLE_WITH_HEAD,
120
277
  slot: bid.slot,
@@ -342,6 +499,29 @@ async function validateExecutionPayloadBid(
342
499
  });
343
500
  }
344
501
 
502
+ // [IGNORE] The parent's payload does not try to exit the builder
503
+ if (byteArrayEquals(bid.parentBlockHash, state.latestExecutionPayloadBid.blockHash)) {
504
+ const requests = await chain.getParentExecutionRequests(parentBlock.slot, parentBlock.blockRoot).catch(() => {
505
+ throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
506
+ code: ExecutionPayloadBidErrorCode.UNKNOWN_PARENT_BLOCK_HASH,
507
+ parentBlockHash: bidParentBlockHash,
508
+ });
509
+ });
510
+ if (
511
+ requests.builderExits.some(
512
+ (request) =>
513
+ byteArrayEquals(request.pubkey, builder.pubkey) &&
514
+ byteArrayEquals(request.sourceAddress, builder.executionAddress)
515
+ )
516
+ ) {
517
+ throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
518
+ code: ExecutionPayloadBidErrorCode.BUILDER_MAY_EXIT,
519
+ builderIndex: bid.builderIndex,
520
+ parentBlockRoot: bidParentBlockRoot,
521
+ });
522
+ }
523
+ }
524
+
345
525
  // [REJECT] `signed_execution_payload_bid.signature` is valid with respect to the `bid.builder_index`.
346
526
  const signatureSet = createSingleSignatureSetFromComponents(
347
527
  builder.pubkey,
@@ -201,6 +201,26 @@ export class BuilderApiClient {
201
201
  }
202
202
  }
203
203
 
204
+ /**
205
+ * Establish the connection to every known builder outside the bid deadline, a cold TCP/TLS handshake
206
+ * to a distant builder can exhaust `BUILDER_BID_DEADLINE_MS` on its own, and a long idle connection
207
+ * may be half-open and only fail once written to.
208
+ */
209
+ async checkStatus(): Promise<void> {
210
+ await Promise.all(
211
+ Array.from(this.clients.entries()).map(async ([url, client]) => {
212
+ try {
213
+ (await client.status()).assertOk();
214
+ this.metrics?.builderApi.statusChecks.inc({status: "success"});
215
+ this.logger?.debug("Builder is ready", {builder: toPrintableUrl(url)});
216
+ } catch (e) {
217
+ this.metrics?.builderApi.statusChecks.inc({status: "error"});
218
+ this.logger?.warn("Builder status check failed", {builder: toPrintableUrl(url)}, e as Error);
219
+ }
220
+ })
221
+ );
222
+ }
223
+
204
224
  private async getOrCreateClient(
205
225
  url: BuilderUrl,
206
226
  proposerPubkey: BLSPubkey,
@@ -237,7 +257,7 @@ export class BuilderApiClient {
237
257
  {config: this.config, metrics: this.metrics?.builderHttpClient, logger: this.logger}
238
258
  );
239
259
  this.clients.set(url, client);
240
- this.logger?.info("External builder registered", {url: toPrintableUrl(url)});
260
+ this.logger?.debug("External builder registered", {url: toPrintableUrl(url)});
241
261
  }
242
262
  return client;
243
263
  }
@@ -37,10 +37,11 @@ export async function validateBuilderApiExecutionPayloadBid(
37
37
  parentBlockHash: RootHex;
38
38
  parentBlockRoot: RootHex;
39
39
  entry: routes.validator.BuilderEntry;
40
+ getParentExecutionRequests: () => Promise<gloas.ExecutionRequests>;
40
41
  }
41
42
  ): Promise<void> {
42
43
  const bid = signedExecutionPayloadBid.message;
43
- const {slot, parentBlock, parentBlockHash, parentBlockRoot, entry} = request;
44
+ const {slot, parentBlock, parentBlockHash, parentBlockRoot, entry, getParentExecutionRequests} = request;
44
45
 
45
46
  if (bid.slot !== slot) {
46
47
  throw Error(`Bid slot=${bid.slot} does not match requested slot=${slot}`);
@@ -118,6 +119,20 @@ export async function validateBuilderApiExecutionPayloadBid(
118
119
  throw Error(`Invalid bid prevRandao=${toHex(bid.prevRandao)} expected=${toHex(randaoMix)}`);
119
120
  }
120
121
 
122
+ // The parent's payload does not try to exit the builder
123
+ if (byteArrayEquals(bid.parentBlockHash, state.latestExecutionPayloadBid.blockHash)) {
124
+ const requests = await getParentExecutionRequests();
125
+ if (
126
+ requests.builderExits.some(
127
+ (request) =>
128
+ byteArrayEquals(request.pubkey, builder.pubkey) &&
129
+ byteArrayEquals(request.sourceAddress, builder.executionAddress)
130
+ )
131
+ ) {
132
+ throw Error(`Bid builderIndex=${bid.builderIndex} may exit in the parent payload`);
133
+ }
134
+ }
135
+
121
136
  // The builder must honor the proposer preferences it learned over gossip
122
137
  const bidEpoch = computeEpochAtSlot(bid.slot);
123
138
  const dependentRootHex = (() => {
@@ -83,15 +83,15 @@ export function createLodestarMetrics(
83
83
  }),
84
84
  jobTime: register.histogram<{topic: GossipType}>({
85
85
  name: "lodestar_gossip_validation_queue_job_time_seconds",
86
- help: "Time to process gossip validation queue job in seconds",
86
+ help: "Time to process gossip validation queue job in seconds (accepted messages only)",
87
87
  labelNames: ["topic"],
88
- buckets: [0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1, 2, 5, 10],
88
+ buckets: [0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1, 2],
89
89
  }),
90
90
  jobWaitTime: register.histogram<{topic: GossipType}>({
91
91
  name: "lodestar_gossip_validation_queue_job_wait_time_seconds",
92
92
  help: "Time from job added to the queue to starting the job in seconds",
93
93
  labelNames: ["topic"],
94
- buckets: [0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1, 2, 5, 10],
94
+ buckets: [0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1, 2],
95
95
  }),
96
96
  concurrency: register.gauge<{topic: GossipType}>({
97
97
  name: "lodestar_gossip_validation_queue_concurrency",
@@ -104,9 +104,10 @@ export function createLodestarMetrics(
104
104
  help: "Age of the first item of each key in the indexed queues in seconds",
105
105
  buckets: [0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1, 5],
106
106
  }),
107
- queueTime: register.histogram({
107
+ queueTime: register.histogram<{topic: GossipType}>({
108
108
  name: "lodestar_gossip_validation_queue_time_seconds",
109
109
  help: "Total time an item stays in queue until it is processed in seconds",
110
+ labelNames: ["topic"],
110
111
  buckets: [0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1, 5],
111
112
  }),
112
113
  },
@@ -920,6 +921,12 @@ export function createLodestarMetrics(
920
921
  help: "Count of errors, by error type, while processing blocks",
921
922
  labelNames: ["error"],
922
923
  }),
924
+
925
+ preStateSource: register.counter<{source: "parentState" | "fallbackPreState" | "preState"}>({
926
+ name: "lodestar_gossip_block_validation_pre_state_source_total",
927
+ help: "Source of the pre-state used for gossip block validation",
928
+ labelNames: ["source"],
929
+ }),
923
930
  },
924
931
  gossipBlob: {
925
932
  recvToValidation: register.histogram({
@@ -1350,6 +1357,11 @@ export function createLodestarMetrics(
1350
1357
  help: "Total number of InsertOutcome as a result of adding an execution payload bid from api to the pool",
1351
1358
  labelNames: ["insertOutcome"],
1352
1359
  }),
1360
+ apiValidationTime: register.histogram({
1361
+ name: "lodestar_api_execution_payload_bid_validation_time_seconds",
1362
+ help: "Time elapsed for signed execution payload bid validation - api path",
1363
+ buckets: [0.005, 0.01, 0.025, 0.05, 0.1, 0.5],
1364
+ }),
1353
1365
  },
1354
1366
  },
1355
1367
 
@@ -2030,6 +2042,11 @@ export function createLodestarMetrics(
2030
2042
  },
2031
2043
 
2032
2044
  builderApi: {
2045
+ statusChecks: register.counter<{status: "success" | "error"}>({
2046
+ name: "lodestar_builder_api_status_checks_total",
2047
+ help: "Total count of status checks sent to external builders ahead of a proposal",
2048
+ labelNames: ["status"],
2049
+ }),
2033
2050
  bidRequests: register.counter({
2034
2051
  name: "lodestar_builder_api_bid_requests_total",
2035
2052
  help: "Total count of execution payload bid requests sent to external builders",
@@ -516,7 +516,8 @@ export class Network implements INetwork {
516
516
 
517
517
  return this.publishGossip<GossipType.execution_payload_bid>(
518
518
  {type: GossipType.execution_payload_bid, boundary},
519
- signedBid
519
+ signedBid,
520
+ {floodPublish: true}
520
521
  );
521
522
  }
522
523
 
@@ -542,17 +543,19 @@ export class Network implements INetwork {
542
543
 
543
544
  private async publishGossip<K extends GossipType>(
544
545
  topic: GossipTopicMap[K],
545
- object: GossipTypeMap[K]
546
+ object: GossipTypeMap[K],
547
+ opts?: PublishOpts
546
548
  ): Promise<number> {
547
549
  const topicStr = stringifyGossipTopic(this.config, topic);
548
550
  const sszType = getGossipSSZType(topic);
549
551
  const messageData = (sszType.serialize as (object: GossipTypeMap[GossipType]) => Uint8Array)(object);
550
- const opts: PublishOpts = {
552
+ const publishOpts: PublishOpts = {
551
553
  ignoreDuplicatePublishError: gossipTopicIgnoreDuplicatePublishError[topic.type],
552
554
  // Leave undefined unless the topic opts out, so `--network.allowPublishToZeroPeers` still applies
553
555
  allowPublishToZeroTopicPeers: gossipTopicAllowPublishToZeroPeers[topic.type] ? true : undefined,
556
+ ...opts,
554
557
  };
555
- const sentPeers = await this.core.publishGossip(topicStr, messageData, opts);
558
+ const sentPeers = await this.core.publishGossip(topicStr, messageData, publishOpts);
556
559
 
557
560
  this.logger.verbose("Publish to topic", {topic: topicStr, sentPeers, currentSlot: this.clock.currentSlot});
558
561
  return sentPeers;
@@ -86,7 +86,6 @@ export class IndexedGossipQueueMinSize<T extends {indexed?: string; queueAddedMs
86
86
  const now = Date.now();
87
87
  // here we mutate item, which is used for gossip validation later
88
88
  item.indexed = key;
89
- item.queueAddedMs = now;
90
89
  let queueItem = this.indexedItems.get(key);
91
90
  if (queueItem == null) {
92
91
  queueItem = {firstSeenMs: now, listItems: new LinkedList<T>()};
@@ -1,3 +1,4 @@
1
+ import {TopicValidatorResult} from "@libp2p/gossipsub";
1
2
  import {routes} from "@lodestar/api";
2
3
  import {ForkSeq} from "@lodestar/params";
3
4
  import {computeStartSlotAtEpoch} from "@lodestar/state-transition";
@@ -560,6 +561,7 @@ export class NetworkProcessor {
560
561
 
561
562
  private pushPendingGossipsubMessageToQueue(message: PendingGossipsubMessage): void {
562
563
  const topicType = message.topic.type;
564
+ message.queueAddedMs = Date.now();
563
565
  const droppedCount = this.gossipQueues[topicType].add(message);
564
566
  if (droppedCount) {
565
567
  // No need to report the dropped job to gossip. It will be eventually pruned from the mcache
@@ -746,12 +748,20 @@ export class NetworkProcessor {
746
748
  for (const msg of messageOrArray) {
747
749
  msg.startProcessUnixSec = nowSec;
748
750
  if (msg.queueAddedMs !== undefined) {
749
- this.metrics?.gossipValidationQueue.queueTime.observe(nowSec - msg.queueAddedMs / 1000);
751
+ this.metrics?.gossipValidationQueue.queueTime.observe(
752
+ {topic: msg.topic.type},
753
+ nowSec - msg.queueAddedMs / 1000
754
+ );
750
755
  }
751
756
  }
752
757
  } else {
753
- // indexed queue is not used here
754
758
  messageOrArray.startProcessUnixSec = nowSec;
759
+ if (messageOrArray.queueAddedMs !== undefined) {
760
+ this.metrics?.gossipValidationQueue.queueTime.observe(
761
+ {topic: messageOrArray.topic.type},
762
+ nowSec - messageOrArray.queueAddedMs / 1000
763
+ );
764
+ }
755
765
  }
756
766
 
757
767
  const acceptanceArr = Array.isArray(messageOrArray)
@@ -764,11 +774,11 @@ export class NetworkProcessor {
764
774
  ];
765
775
 
766
776
  if (Array.isArray(messageOrArray)) {
767
- for (const msg of messageOrArray) {
768
- this.trackJobTime(msg, messageOrArray.length);
777
+ for (const [i, msg] of messageOrArray.entries()) {
778
+ this.trackJobTime(msg, messageOrArray.length, acceptanceArr[i]);
769
779
  }
770
780
  } else {
771
- this.trackJobTime(messageOrArray, 1);
781
+ this.trackJobTime(messageOrArray, 1, acceptanceArr[0]);
772
782
  }
773
783
 
774
784
  // Use setTimeout to yield to the macro queue
@@ -796,17 +806,21 @@ export class NetworkProcessor {
796
806
  }
797
807
  }
798
808
 
799
- private trackJobTime(message: PendingGossipsubMessage, numJob: number): void {
809
+ private trackJobTime(message: PendingGossipsubMessage, numJob: number, acceptance: TopicValidatorResult): void {
800
810
  if (message.startProcessUnixSec !== null) {
811
+ // record job wait time for all messages
801
812
  this.metrics?.gossipValidationQueue.jobWaitTime.observe(
802
813
  {topic: message.topic.type},
803
814
  message.startProcessUnixSec - message.seenTimestampSec
804
815
  );
805
- // if it takes 64ms to process 64 jobs, the average job time is 1ms
806
- this.metrics?.gossipValidationQueue.jobTime.observe(
807
- {topic: message.topic.type},
808
- (Date.now() / 1000 - message.startProcessUnixSec) / numJob
809
- );
816
+ // but only record job time for ACCEPT messages
817
+ if (acceptance === TopicValidatorResult.Accept) {
818
+ // if it takes 64ms to process 64 jobs, the average job time is 1ms
819
+ this.metrics?.gossipValidationQueue.jobTime.observe(
820
+ {topic: message.topic.type},
821
+ (Date.now() / 1000 - message.startProcessUnixSec) / numJob
822
+ );
823
+ }
810
824
  }
811
825
  }
812
826
 
@@ -652,7 +652,9 @@ export class SyncChain {
652
652
 
653
653
  const blockSlots = downloadSuccessOutput.blocks.map((b) => b.slot);
654
654
  const envelopeSlots = downloadSuccessOutput.payloadEnvelopes
655
- ? Array.from(downloadSuccessOutput.payloadEnvelopes.keys())
655
+ ? Array.from(downloadSuccessOutput.payloadEnvelopes.entries())
656
+ .filter(([, payloadInput]) => payloadInput.hasPayloadEnvelope())
657
+ .map(([slot]) => slot)
656
658
  : null;
657
659
 
658
660
  this.logger.debug(logMessage, {
@@ -684,13 +686,18 @@ export class SyncChain {
684
686
  */
685
687
  private async processBatch(batch: Batch): Promise<void> {
686
688
  const {blocks, payloadEnvelopes, peers} = batch.startProcessing();
689
+ const envelopeSlots = payloadEnvelopes
690
+ ? Array.from(payloadEnvelopes.entries())
691
+ .filter(([, payloadInput]) => payloadInput.hasPayloadEnvelope())
692
+ .map(([slot]) => slot)
693
+ : null;
687
694
 
688
695
  const logCtx = {
689
696
  id: this.logId,
690
697
  ...batch.getMetadata(),
691
698
  blockCount: blocks.length,
692
699
  blockSlots: prettyPrintIndices(blocks.map((b) => b.slot)),
693
- ...(payloadEnvelopes ? {envelopeSlots: prettyPrintIndices(Array.from(payloadEnvelopes.keys()))} : {}),
700
+ ...(envelopeSlots ? {envelopeSlots: prettyPrintIndices(envelopeSlots)} : {}),
694
701
  peers: peers.map(prettyPrintPeerIdStr).join(","),
695
702
  };
696
703
  this.logger.verbose("Processing batch", logCtx);
@@ -212,7 +212,7 @@ export class RangeSync extends (EventEmitter as {new (): RangeSyncEmitter}) {
212
212
  for (const block of blocks) {
213
213
  await this.chain.processBlock(block, flags);
214
214
  const payloadEnvelope = payloadEnvelopes?.get(block.slot);
215
- if (payloadEnvelope) {
215
+ if (payloadEnvelope?.hasPayloadEnvelope()) {
216
216
  await this.chain.processExecutionPayload(payloadEnvelope);
217
217
  }
218
218
  }