@lodestar/beacon-node 1.51.0-dev.3fada841a9 → 1.51.0-dev.3ff306e42b

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 (59) hide show
  1. package/lib/api/impl/validator/index.d.ts +6 -0
  2. package/lib/api/impl/validator/index.d.ts.map +1 -1
  3. package/lib/api/impl/validator/index.js +38 -25
  4. package/lib/api/impl/validator/index.js.map +1 -1
  5. package/lib/chain/blocks/types.d.ts +4 -1
  6. package/lib/chain/blocks/types.d.ts.map +1 -1
  7. package/lib/chain/blocks/verifyBlock.d.ts.map +1 -1
  8. package/lib/chain/blocks/verifyBlock.js +17 -6
  9. package/lib/chain/blocks/verifyBlock.js.map +1 -1
  10. package/lib/chain/blocks/verifyBlocksSignatures.d.ts.map +1 -1
  11. package/lib/chain/blocks/verifyBlocksSignatures.js +7 -5
  12. package/lib/chain/blocks/verifyBlocksSignatures.js.map +1 -1
  13. package/lib/chain/bls/interface.d.ts +2 -0
  14. package/lib/chain/bls/interface.d.ts.map +1 -1
  15. package/lib/chain/bls/multithread/index.d.ts.map +1 -1
  16. package/lib/chain/bls/multithread/index.js +8 -6
  17. package/lib/chain/bls/multithread/index.js.map +1 -1
  18. package/lib/execution/builder/apiClient.d.ts +14 -8
  19. package/lib/execution/builder/apiClient.d.ts.map +1 -1
  20. package/lib/execution/builder/apiClient.js +12 -10
  21. package/lib/execution/builder/apiClient.js.map +1 -1
  22. package/lib/execution/builder/http.d.ts +2 -0
  23. package/lib/execution/builder/http.d.ts.map +1 -1
  24. package/lib/execution/builder/http.js +2 -0
  25. package/lib/execution/builder/http.js.map +1 -1
  26. package/lib/metrics/metrics/lodestar.d.ts +1 -0
  27. package/lib/metrics/metrics/lodestar.d.ts.map +1 -1
  28. package/lib/metrics/metrics/lodestar.js +5 -0
  29. package/lib/metrics/metrics/lodestar.js.map +1 -1
  30. package/lib/metrics/server/http.js +5 -5
  31. package/lib/metrics/server/http.js.map +1 -1
  32. package/lib/network/libp2p/index.d.ts.map +1 -1
  33. package/lib/network/libp2p/index.js +5 -1
  34. package/lib/network/libp2p/index.js.map +1 -1
  35. package/lib/network/options.d.ts +2 -0
  36. package/lib/network/options.d.ts.map +1 -1
  37. package/lib/network/options.js.map +1 -1
  38. package/lib/node/nodejs.d.ts.map +1 -1
  39. package/lib/node/nodejs.js +1 -0
  40. package/lib/node/nodejs.js.map +1 -1
  41. package/lib/node/options.d.ts +3 -1
  42. package/lib/node/options.d.ts.map +1 -1
  43. package/lib/node/options.js +4 -2
  44. package/lib/node/options.js.map +1 -1
  45. package/package.json +16 -16
  46. package/src/api/impl/validator/index.ts +43 -29
  47. package/src/chain/blocks/types.ts +4 -1
  48. package/src/chain/blocks/verifyBlock.ts +30 -16
  49. package/src/chain/blocks/verifyBlocksSignatures.ts +7 -5
  50. package/src/chain/bls/interface.ts +2 -0
  51. package/src/chain/bls/multithread/index.ts +9 -5
  52. package/src/execution/builder/apiClient.ts +16 -13
  53. package/src/execution/builder/http.ts +4 -0
  54. package/src/metrics/metrics/lodestar.ts +5 -0
  55. package/src/metrics/server/http.ts +5 -5
  56. package/src/network/libp2p/index.ts +5 -1
  57. package/src/network/options.ts +2 -0
  58. package/src/node/nodejs.ts +1 -0
  59. package/src/node/options.ts +5 -1
package/package.json CHANGED
@@ -11,7 +11,7 @@
11
11
  "bugs": {
12
12
  "url": "https://github.com/ChainSafe/lodestar/issues"
13
13
  },
14
- "version": "1.51.0-dev.3fada841a9",
14
+ "version": "1.51.0-dev.3ff306e42b",
15
15
  "type": "module",
16
16
  "exports": {
17
17
  ".": {
@@ -126,18 +126,18 @@
126
126
  "@libp2p/prometheus-metrics": "^5.0.31",
127
127
  "@libp2p/tcp": "^11.0.30",
128
128
  "@libp2p/utils": "^7.4.3",
129
- "@lodestar/api": "^1.51.0-dev.3fada841a9",
130
- "@lodestar/config": "^1.51.0-dev.3fada841a9",
131
- "@lodestar/db": "^1.51.0-dev.3fada841a9",
132
- "@lodestar/fork-choice": "^1.51.0-dev.3fada841a9",
133
- "@lodestar/logger": "^1.51.0-dev.3fada841a9",
134
- "@lodestar/native": "^1.51.0-dev.3fada841a9",
135
- "@lodestar/params": "^1.51.0-dev.3fada841a9",
136
- "@lodestar/reqresp": "^1.51.0-dev.3fada841a9",
137
- "@lodestar/state-transition": "^1.51.0-dev.3fada841a9",
138
- "@lodestar/types": "^1.51.0-dev.3fada841a9",
139
- "@lodestar/utils": "^1.51.0-dev.3fada841a9",
140
- "@lodestar/validator": "^1.51.0-dev.3fada841a9",
129
+ "@lodestar/api": "^1.51.0-dev.3ff306e42b",
130
+ "@lodestar/config": "^1.51.0-dev.3ff306e42b",
131
+ "@lodestar/db": "^1.51.0-dev.3ff306e42b",
132
+ "@lodestar/fork-choice": "^1.51.0-dev.3ff306e42b",
133
+ "@lodestar/logger": "^1.51.0-dev.3ff306e42b",
134
+ "@lodestar/native": "^1.51.0-dev.3ff306e42b",
135
+ "@lodestar/params": "^1.51.0-dev.3ff306e42b",
136
+ "@lodestar/reqresp": "^1.51.0-dev.3ff306e42b",
137
+ "@lodestar/state-transition": "^1.51.0-dev.3ff306e42b",
138
+ "@lodestar/types": "^1.51.0-dev.3ff306e42b",
139
+ "@lodestar/utils": "^1.51.0-dev.3ff306e42b",
140
+ "@lodestar/validator": "^1.51.0-dev.3ff306e42b",
141
141
  "@multiformats/multiaddr": "^13.0.1",
142
142
  "datastore-core": "^12.0.1",
143
143
  "datastore-level": "^13.0.1",
@@ -159,8 +159,8 @@
159
159
  "@ethereumjs/rlp": "^4.0.1",
160
160
  "@libp2p/interface-internal": "^3.2.0",
161
161
  "@libp2p/logger": "^6.2.15",
162
- "@lodestar/spec-test-util": "^1.51.0-dev.3fada841a9",
163
- "@lodestar/test-utils": "^1.51.0-dev.3fada841a9",
162
+ "@lodestar/spec-test-util": "^1.51.0-dev.3ff306e42b",
163
+ "@lodestar/test-utils": "^1.51.0-dev.3ff306e42b",
164
164
  "@types/js-yaml": "^4.0.5",
165
165
  "@types/qs": "^6.9.7",
166
166
  "@types/tmp": "^0.2.3",
@@ -174,5 +174,5 @@
174
174
  "beacon",
175
175
  "blockchain"
176
176
  ],
177
- "gitHead": "2707b5a195a0edd60c5c4fab2472cf4ca2e689f5"
177
+ "gitHead": "dff90dd1d729df6da6a98c1d2f5f1fd7408d5846"
178
178
  }
@@ -94,7 +94,7 @@ import {
94
94
  import {validateGossipProposerPreferences} from "../../../chain/validation/proposerPreferences.js";
95
95
  import {validateSyncCommitteeGossipContributionAndProof} from "../../../chain/validation/syncCommitteeContributionAndProof.js";
96
96
  import {ZERO_HASH} from "../../../constants/index.js";
97
- import {BUILDER_BID_DEADLINE_MS, BuilderApiBid, decodeBuilderUrl} from "../../../execution/builder/apiClient.js";
97
+ import {BuilderApiBid, decodeBuilderUrl} from "../../../execution/builder/apiClient.js";
98
98
  import {BuilderStatus, NoBidReceived} from "../../../execution/builder/http.js";
99
99
  import {getBuilderBidTotalGwei, validateBuilderApiExecutionPayloadBid} from "../../../execution/builder/validateBid.js";
100
100
  import {validateGossipFnRetryUnknownRoot} from "../../../network/processor/gossipHandlers.js";
@@ -123,10 +123,14 @@ import {
123
123
  * A cutoff of 1667 BPS (2 seconds for 12s slots) gives enough time and if there are unexpected delays
124
124
  * it ensures we publish in time as proposals after the attestation deadline will likely be orphaned
125
125
  * due to proposer boost reorg.
126
- *
127
- * TODO GLOAS: re-evaluate cutoff timing due to attestation deadline changes in gloas
128
126
  */
129
127
  const BLOCK_PRODUCTION_RACE_CUTOFF_BPS = 1667;
128
+ /**
129
+ * Post-gloas cutoff, kept at 1667 BPS (2 seconds for 12s slots) for now.
130
+ *
131
+ * TODO GLOAS: revisit, the block propagates faster as it only commits to a bid and the attestation deadline changed
132
+ */
133
+ export const BLOCK_PRODUCTION_RACE_CUTOFF_GLOAS_BPS = 1667;
130
134
  /** Rejection message of the bid block branch when there is no viable bid to commit to */
131
135
  const NO_BID_AVAILABLE = "No builder bid available";
132
136
 
@@ -1005,33 +1009,50 @@ export function getValidatorApi(
1005
1009
  (!circuitBreakerActive &&
1006
1010
  chain.executionPayloadBidPool.getBestBid(slot, bidParentBlockHash, parentBlockRootHex) !== null);
1007
1011
 
1008
- // Select the p2p bid once builders had time to bid up, matching the deadline advertised
1009
- // on builder API bid requests, unless the circuit breaker is active
1010
- const p2pBidPromise: Promise<PooledExecutionPayloadBid | null> = circuitBreakerActive
1011
- ? Promise.resolve(null)
1012
- : sleep(Math.max(0, BUILDER_BID_DEADLINE_MS - chain.clock.msFromSlot(slot))).then(() => {
1013
- const p2pBid = chain.executionPayloadBidPool.getBestBid(slot, bidParentBlockHash, parentBlockRootHex);
1014
- // Discard p2p bids below the proposer's configured floor on the total payment.
1015
- // A p2p bid's total is just its value since gossip validation enforces executionPayment=0.
1016
- if (p2pBid !== null && BigInt(p2pBid.signedBid.message.value) < builderConfig.minBid) {
1017
- logger.info("Best p2p bid below configured minimum", {
1018
- slot,
1019
- bidValue: prettyGweiToEth(p2pBid.signedBid.message.value),
1020
- minBid: prettyGweiToEth(builderConfig.minBid),
1021
- });
1022
- return null;
1023
- }
1024
- return p2pBid;
1025
- });
1012
+ // Calculate cutoff time based on start of the slot, ensures a slow local payload build does
1013
+ // not delay the proposal when a builder bid block is available (and vice versa)
1014
+ const cutoffMs = Math.max(
1015
+ 0,
1016
+ config.getSlotComponentDurationMs(fork, BLOCK_PRODUCTION_RACE_CUTOFF_GLOAS_BPS) - chain.clock.msFromSlot(slot)
1017
+ );
1018
+
1019
+ // Give p2p bids the same time to bid up as granted to builder API bid requests via the advertised
1020
+ // deadline, the pool is read once the deadline passed and the bid requests settled. A late request
1021
+ // does not wait past the race cutoff, a pooled bid is still the fastest fallback if the local block fails
1022
+ const bidDeadlinePromise = circuitBreakerActive
1023
+ ? Promise.resolve()
1024
+ : sleep(Math.min(chain.builderApiClient.bidDeadline, cutoffMs));
1026
1025
 
1027
1026
  // Candidates are ranked by their boosted counted total payment, the p2p bid is governed
1028
1027
  // by the top-level factors and each builder API bid by its own entry. Ties prefer the
1029
1028
  // earliest received builder API bid, so the signed block can be routed back to its
1030
1029
  // builder directly.
1031
1030
  const bestBidPromise: Promise<BidCandidate | null> = (async () => {
1032
- const [builderApiBids, p2pBid] = await Promise.all([builderApiBidsPromise, p2pBidPromise]);
1031
+ // Past the race cutoff a pooled p2p bid is used right away rather than waiting for pending
1032
+ // builder API requests, the local block is selected at the cutoff anyway if it is ready
1033
+ let [builderApiBids] = await Promise.all([
1034
+ Promise.race([builderApiBidsPromise, sleep(cutoffMs).then(() => null)]),
1035
+ bidDeadlinePromise,
1036
+ ]);
1033
1037
  let parentExecutionRequestsPromise: Promise<gloas.ExecutionRequests> | null = null;
1034
1038
 
1039
+ let p2pBid: PooledExecutionPayloadBid | null = circuitBreakerActive
1040
+ ? null
1041
+ : chain.executionPayloadBidPool.getBestBid(slot, bidParentBlockHash, parentBlockRootHex);
1042
+ // Discard p2p bids below the proposer's configured floor on the total payment.
1043
+ // A p2p bid's total is just its value since gossip validation enforces executionPayment=0.
1044
+ if (p2pBid !== null && BigInt(p2pBid.signedBid.message.value) < builderConfig.minBid) {
1045
+ logger.info("Best p2p bid below configured minimum", {
1046
+ slot,
1047
+ bidValue: prettyGweiToEth(p2pBid.signedBid.message.value),
1048
+ minBid: prettyGweiToEth(builderConfig.minBid),
1049
+ });
1050
+ p2pBid = null;
1051
+ }
1052
+ if (builderApiBids === null) {
1053
+ builderApiBids = p2pBid !== null ? [] : await builderApiBidsPromise;
1054
+ }
1055
+
1035
1056
  const candidates = (
1036
1057
  await Promise.all(
1037
1058
  builderApiBids.map(async ({url, entry, signedBid, receivedMs}): Promise<BidCandidate | null> => {
@@ -1118,13 +1139,6 @@ export function getValidatorApi(
1118
1139
  return fn().finally(() => t?.({source}));
1119
1140
  };
1120
1141
 
1121
- // Calculate cutoff time based on start of the slot, ensures a slow local payload build does
1122
- // not delay the proposal when a builder bid block is available (and vice versa)
1123
- const cutoffMs = Math.max(
1124
- 0,
1125
- config.getSlotComponentDurationMs(fork, BLOCK_PRODUCTION_RACE_CUTOFF_BPS) - chain.clock.msFromSlot(slot)
1126
- );
1127
-
1128
1142
  // use abort controller to stop waiting for the bid block if the engine block will be selected
1129
1143
  const controller = new AbortController();
1130
1144
 
@@ -109,7 +109,10 @@ export type FullyVerifiedBlock = {
109
109
  parentBlockSlot: Slot;
110
110
  proposerBalanceDelta: number;
111
111
  dataAvailabilityStatus: DataAvailabilityStatus;
112
- /** Pre-computed indexed attestations from signature verification to avoid duplicate work */
112
+ /**
113
+ * Pre-computed indexed attestations from signature verification to avoid duplicate work.
114
+ * Their `attestingIndices` are in committee order, not sorted, so they are not spec-valid `IndexedAttestation`s.
115
+ */
113
116
  indexedAttestations: IndexedAttestation[];
114
117
  /** Seen timestamp seconds */
115
118
  seenTimestampSec: number;
@@ -4,7 +4,7 @@ import {
4
4
  DataAvailabilityStatus,
5
5
  IBeaconStateView,
6
6
  computeEpochAtSlot,
7
- getIndexedAttestation,
7
+ getAttestingIndices,
8
8
  signedBlockToSignedHeader,
9
9
  } from "@lodestar/state-transition";
10
10
  import {IndexedAttestation, Slot, deneb} from "@lodestar/types";
@@ -119,7 +119,13 @@ export async function verifyBlocksInEpoch(
119
119
  const shuffling =
120
120
  this.shufflingCache.getSync(attEpoch, preState0.getShufflingDecisionRoot(attEpoch)) ??
121
121
  preState0.getShufflingAtEpoch(attEpoch);
122
- return getIndexedAttestation(shuffling, fork, attestation);
122
+ // Unlike getIndexedAttestation, keep attestingIndices in committee order: sorting them dominates building
123
+ // these on mainnet blocks. See FullyVerifiedBlock.indexedAttestations.
124
+ return {
125
+ attestingIndices: getAttestingIndices(shuffling, fork, attestation),
126
+ data: attestation.data,
127
+ signature: attestation.signature,
128
+ };
123
129
  });
124
130
  }
125
131
 
@@ -175,16 +181,29 @@ export async function verifyBlocksInEpoch(
175
181
  const [
176
182
  segmentExecStatus,
177
183
  {blockDAStatuses, payloadDAStatuses, availableTime},
178
- {postStates, proposerBalanceDeltas, verifyStateTime},
179
184
  {verifySignaturesTime},
185
+ {postStates, proposerBalanceDeltas, verifyStateTime},
180
186
  ] = await Promise.all([
181
187
  verifyExecutionPayloadsPromise,
182
188
 
183
189
  // data availability (fork-specific; see daAvailabilityPromise above)
184
190
  daAvailabilityPromise,
185
191
 
192
+ // All signatures at once. Must be called before verifyBlocksStateTransitionOnly()
193
+ // so we hand the BLS job to the worker pool before the main thread
194
+ // is busy with the state transition.
195
+ verifyBlocksSignatures(
196
+ this.config,
197
+ this.bls,
198
+ this.logger,
199
+ this.metrics,
200
+ preState0,
201
+ blocks,
202
+ indexedAttestationsByBlock,
203
+ opts
204
+ ),
205
+
186
206
  // Run state transition only
187
- // TODO: Ensure it yields to allow flushing to workers and engine API
188
207
  verifyBlocksStateTransitionOnly(
189
208
  preState0,
190
209
  blockInputs,
@@ -198,22 +217,17 @@ export async function verifyBlocksInEpoch(
198
217
  opts
199
218
  ),
200
219
 
201
- // All signatures at once
202
- verifyBlocksSignatures(
203
- this.config,
204
- this.bls,
205
- this.logger,
206
- this.metrics,
207
- preState0,
208
- blocks,
209
- indexedAttestationsByBlock,
210
- opts
211
- ),
212
-
213
220
  // TODO GLOAS: can verify payload signatures in batch too
214
221
  // maybe chain with the above verifyBlocksSignatures()
215
222
  ]);
216
223
 
224
+ if (blockInputs.length === 1 && opts.seenTimestampSec !== undefined) {
225
+ const consensusVerifiedTime = Math.max(verifyStateTime, verifySignaturesTime);
226
+ this.metrics?.gossipBlock.receivedToConsensusVerifiedTime.observe(
227
+ consensusVerifiedTime / 1000 - opts.seenTimestampSec
228
+ );
229
+ }
230
+
217
231
  for (const blockInput of blockInputs) {
218
232
  const block = blockInput.getBlock();
219
233
  const {slot, proposerIndex} = block.message;
@@ -36,10 +36,9 @@ export async function verifyBlocksSignatures(
36
36
  ? preState0.currentSyncCommitteeIndexed
37
37
  : new SyncCommitteeCacheEmpty();
38
38
 
39
- // Verifies signatures after running state transition, so all SyncCommittee signed roots are known at this point.
40
- // We must ensure block.slot <= state.slot before running getAllBlockSignatureSets().
41
- // NOTE: If in the future multiple blocks signatures are verified at once, all blocks must be in the same epoch
42
- // so the attester and proposer shufflings are correct.
39
+ // Runs concurrently with the state transition. Signature sets are built from preState0, which
40
+ // verifyBlocksInEpoch() guarantees is in the same epoch as every block, so the proposer and attester shufflings
41
+ // and the sync committee are the ones the blocks were signed under.
43
42
  for (const [i, block] of blocks.entries()) {
44
43
  // Use [i] to make clear that the index has to be correct to blame the right block below on BlockError()
45
44
  isValidPromises[i] = opts.validSignatures
@@ -50,7 +49,10 @@ export async function verifyBlocksSignatures(
50
49
  bls.verifySignatureSets(
51
50
  getBlockSignatureSets(config, currentSyncCommitteeIndexed, preState0, block, indexedAttestationsByBlock[i], {
52
51
  skipProposerSignature: opts.validProposerSignature,
53
- })
52
+ }),
53
+ // Prioritize block verification over attestations, since
54
+ // attestations already buffer up to `MAX_BUFFER_WAIT_MS`
55
+ {priority: true}
54
56
  );
55
57
 
56
58
  // getBlockSignatureSets() takes 45ms in benchmarks for 2022Q2 mainnet blocks (100 sigs). When syncing a 32 blocks
@@ -22,6 +22,8 @@ export type VerifySignatureOpts = {
22
22
  verifyOnMainThread?: boolean;
23
23
  /**
24
24
  * Some signature sets are more important than others, and should be verified first.
25
+ * Non-batchable priority jobs are posted to an idle worker before this call returns, so the caller
26
+ * can run synchronous work right after without delaying the verification.
25
27
  */
26
28
  priority?: boolean;
27
29
  };
@@ -333,15 +333,19 @@ export class BlsMultiThreadWorkerPool implements IBlsVerifier {
333
333
  }
334
334
  }
335
335
 
336
+ // First hand work to an idle worker before returning to the caller.
337
+ // Deferring the post to the next macrotask would make the worker
338
+ // sit idle while waiting for the next synchronous work
339
+ else if (job.opts.priority) {
340
+ this.jobs.unshift(job);
341
+ void this.runJob();
342
+ }
343
+
336
344
  // Push job and schedule to call `runJob` in the next macro event loop cycle.
337
345
  // This is useful to allow batching job submitted from a synchronous for loop,
338
346
  // and to prevent large stacks since runJob may be called recursively.
339
347
  else {
340
- if (job.opts.priority) {
341
- this.jobs.unshift(job);
342
- } else {
343
- this.jobs.push(job);
344
- }
348
+ this.jobs.push(job);
345
349
  callInNextEventLoop(() => void this.runJob());
346
350
  }
347
351
  }
@@ -19,21 +19,20 @@ import {IClock} from "../../util/clock.js";
19
19
  * Additional duration to account for potential event loop lag which causes
20
20
  * builder bids to be rejected even though the response was sent in time.
21
21
  */
22
- const EVENT_LOOP_LAG_BUFFER = 250;
22
+ export const BUILDER_BID_LAG_BUFFER_MS = 250;
23
23
 
24
24
  /**
25
- * Duration given to a builder to provide a `SignedExecutionPayloadBid` before the deadline
26
- * is reached, advertised on each bid request via the `X-Timeout-Ms` header. The p2p bid is
27
- * selected after the same duration, only considering bids received up to that point.
25
+ * Default duration given to a builder to provide a `SignedExecutionPayloadBid` before the deadline
26
+ * is reached, advertised on each bid request via the `X-Timeout-Ms` header. Bids received via
27
+ * p2p are given the same time to bid up.
28
28
  */
29
- export const BUILDER_BID_DEADLINE_MS = 500;
30
-
31
- /** Local timeout for bid requests, event loop lag must not discard bids that arrived in time */
32
- export const BUILDER_BID_REQUEST_TIMEOUT_MS = BUILDER_BID_DEADLINE_MS + EVENT_LOOP_LAG_BUFFER;
29
+ export const DEFAULT_BUILDER_BID_DEADLINE_MS = 500;
33
30
 
34
31
  export type BuilderApiClientOpts = {
35
- /** Timeout for builder api requests, bid requests always use `BUILDER_BID_REQUEST_TIMEOUT_MS` */
32
+ /** Timeout for builder api requests, bid requests are bounded by `bidDeadline` instead */
36
33
  timeout?: number;
34
+ /** Time builders are given to provide a bid once block production starts, in milliseconds */
35
+ bidDeadline?: number;
37
36
  // Add User-Agent header to all requests
38
37
  userAgent?: string;
39
38
  };
@@ -69,6 +68,7 @@ export type BuilderApiBid = {
69
68
  * each block production request, clients are dialed on demand based on the entry `url`.
70
69
  */
71
70
  export class BuilderApiClient {
71
+ readonly bidDeadline: number;
72
72
  private readonly clients = new Map<BuilderUrl, BuilderApi>();
73
73
 
74
74
  constructor(
@@ -78,7 +78,9 @@ export class BuilderApiClient {
78
78
  private readonly bls: IBlsVerifier,
79
79
  private readonly metrics: Metrics | null = null,
80
80
  private readonly logger?: Logger
81
- ) {}
81
+ ) {
82
+ this.bidDeadline = opts.bidDeadline ?? DEFAULT_BUILDER_BID_DEADLINE_MS;
83
+ }
82
84
 
83
85
  /**
84
86
  * Fan out bid requests to the builders named by the entries, one request per unique
@@ -139,9 +141,10 @@ export class BuilderApiClient {
139
141
  proposerPubkey,
140
142
  requestAuth: entry.auth,
141
143
  dateMilliseconds: Date.now(),
142
- timeoutMs: BUILDER_BID_DEADLINE_MS,
144
+ timeoutMs: this.bidDeadline,
143
145
  },
144
- {timeoutMs: BUILDER_BID_REQUEST_TIMEOUT_MS}
146
+ // Event loop lag must not discard bids that arrived in time
147
+ {timeoutMs: this.bidDeadline + BUILDER_BID_LAG_BUFFER_MS}
145
148
  );
146
149
  const signedBid = res.value();
147
150
  if (signedBid === undefined) {
@@ -203,7 +206,7 @@ export class BuilderApiClient {
203
206
 
204
207
  /**
205
208
  * 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
209
+ * to a distant builder can exhaust the bid deadline on its own, and a long idle connection
207
210
  * may be half-open and only fail once written to.
208
211
  */
209
212
  async checkStatus(): Promise<void> {
@@ -20,6 +20,7 @@ import {
20
20
  } from "@lodestar/types";
21
21
  import {toPrintableUrl} from "@lodestar/utils";
22
22
  import {Metrics} from "../../metrics/metrics.js";
23
+ import {DEFAULT_BUILDER_BID_DEADLINE_MS} from "./apiClient.js";
23
24
  import {ValidatorRegistration, ValidatorRegistrationCache} from "./cache.js";
24
25
  import {IExecutionBuilder} from "./interface.js";
25
26
 
@@ -29,6 +30,8 @@ export type ExecutionBuilderHttpOpts = {
29
30
  timeout?: number;
30
31
  faultInspectionWindow?: number;
31
32
  allowedFaults?: number;
33
+ /** Post-gloas only, time builders are given to provide a bid once block production starts, in milliseconds */
34
+ bidDeadline?: number;
32
35
 
33
36
  // Only required for merge-mock runs, no need to expose it to cli
34
37
  issueLocalFcUWithFeeRecipient?: string;
@@ -40,6 +43,7 @@ export const defaultExecutionBuilderHttpOpts: ExecutionBuilderHttpOpts = {
40
43
  enabled: false,
41
44
  url: "http://localhost:8661",
42
45
  timeout: 12000,
46
+ bidDeadline: DEFAULT_BUILDER_BID_DEADLINE_MS,
43
47
  };
44
48
 
45
49
  export enum BuilderStatus {
@@ -1013,6 +1013,11 @@ export function createLodestarMetrics(
1013
1013
  help: "Time elapsed between block received and fully verified state, signatures and payload",
1014
1014
  buckets: [0.05, 0.1, 0.3, 0.5, 0.7, 1, 1.3, 1.6, 2, 2.5, 3, 3.5, 4],
1015
1015
  }),
1016
+ receivedToConsensusVerifiedTime: register.histogram({
1017
+ name: "lodestar_gossip_block_received_to_consensus_verified_seconds",
1018
+ help: "Time from gossip block arrival until both the state transition and all block signature verifications are complete, excluding execution payload verification",
1019
+ buckets: [0.01, 0.015, 0.02, 0.025, 0.03, 0.04, 0.5, 1],
1020
+ }),
1016
1021
  verifiedToBlobsAvailabiltyTime: register.histogram<{numBlobs: number}>({
1017
1022
  name: "lodestar_gossip_block_verified_to_blobs_availability_time",
1018
1023
  help: "Time elapsed between block verified and blobs became available",
@@ -44,16 +44,16 @@ export async function getHttpMetricsServer(
44
44
  ): Promise<void> {
45
45
  if (req.method === "GET" && req.url && req.url.includes("/metrics")) {
46
46
  const timer = scrapeTimeMetric.startTimer();
47
- const metricsRes = await Promise.all([wrapError(register.metrics()), getOtherMetrics()]);
48
- timer({status: metricsRes[0].err ? RequestStatus.error : RequestStatus.success});
47
+ const metricsRes = await wrapError(Promise.all([register.metrics(), getOtherMetrics()]));
48
+ timer({status: metricsRes.err ? RequestStatus.error : RequestStatus.success});
49
49
 
50
50
  // Ensure we only writeHead once
51
- if (metricsRes[0].err) {
52
- res.writeHead(500, {"content-type": "text/plain"}).end(metricsRes[0].err.stack);
51
+ if (metricsRes.err) {
52
+ res.writeHead(500, {"content-type": "text/plain"}).end(metricsRes.err.stack);
53
53
  } else {
54
54
  // Get scrape time metrics
55
55
  const httpServerMetrics = await httpServerRegister.metrics();
56
- const metrics = [metricsRes[0].result, httpServerMetrics, ...metricsRes[1]];
56
+ const metrics = [metricsRes.result[0], httpServerMetrics, ...metricsRes.result[1]];
57
57
  const metricsStr = metrics.join("\n\n");
58
58
  res.writeHead(200, {"content-type": register.contentType}).end(metricsStr);
59
59
  }
@@ -150,7 +150,11 @@ export async function createNodeJsLibp2p(
150
150
  nodeInfo: {
151
151
  name: "lodestar",
152
152
  version: networkOpts.version ?? "unknown",
153
- userAgent: networkOpts.private ? "" : networkOpts.version ? `lodestar/${networkOpts.version}` : "lodestar",
153
+ userAgent: networkOpts.private
154
+ ? ""
155
+ : networkOpts.advertiseVersion && networkOpts.version
156
+ ? `lodestar/${networkOpts.version}`
157
+ : "lodestar",
154
158
  },
155
159
  addresses: {
156
160
  listen: localMultiaddrs,
@@ -25,6 +25,8 @@ export interface NetworkOptions
25
25
  mdns?: boolean;
26
26
  connectToDiscv5Bootnodes?: boolean;
27
27
  version?: string;
28
+ /** Include the version in libp2p Identify. Private mode takes precedence. */
29
+ advertiseVersion?: boolean;
28
30
  private?: boolean;
29
31
  useWorker?: boolean;
30
32
  maxYoungGenerationSizeMb?: number;
@@ -297,6 +297,7 @@ export class BeaconNode {
297
297
  : undefined,
298
298
  builderApiClientOpts: {
299
299
  timeout: opts.executionBuilder.timeout,
300
+ bidDeadline: opts.executionBuilder.bidDeadline,
300
301
  // Sent with all builder api requests, unless the node runs in private mode
301
302
  userAgent: opts.executionBuilder.userAgent,
302
303
  },
@@ -1,7 +1,9 @@
1
+ import {BLOCK_PRODUCTION_RACE_CUTOFF_GLOAS_BPS} from "../api/impl/validator/index.js";
1
2
  import {ApiOptions, defaultApiOptions} from "../api/options.js";
2
3
  import {ArchiveMode, DEFAULT_ARCHIVE_MODE, IChainOptions, defaultChainOptions} from "../chain/options.js";
3
4
  import {ValidatorMonitorOpts, defaultValidatorMonitorOpts} from "../chain/validatorMonitor.js";
4
5
  import {DatabaseOptions, defaultDbOptions} from "../db/options.js";
6
+ import {BUILDER_BID_LAG_BUFFER_MS} from "../execution/builder/apiClient.js";
5
7
  import {
6
8
  EngineApiMode,
7
9
  ExecutionBuilderOpts,
@@ -20,9 +22,11 @@ import {SyncOptions, defaultSyncOptions} from "../sync/options.js";
20
22
  // Re-export so the CLI doesn't need to depend on lodestar-api
21
23
  export {allNamespaces} from "../api/rest/index.js";
22
24
  export type {EngineApiMode};
23
- // Re-export to use as default values in CLI args
25
+ // Re-export to use as default values and for validation in CLI args
24
26
  export {
25
27
  ArchiveMode,
28
+ BLOCK_PRODUCTION_RACE_CUTOFF_GLOAS_BPS,
29
+ BUILDER_BID_LAG_BUFFER_MS,
26
30
  DEFAULT_ARCHIVE_MODE,
27
31
  defaultExecutionBuilderHttpOpts,
28
32
  defaultExecutionEngineHttpOpts,