@lodestar/beacon-node 1.51.0-dev.d422f387ac → 1.51.0-dev.e21949cfcf

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 (89) 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/chain/errors/executionPayloadEnvelope.d.ts +7 -0
  19. package/lib/chain/errors/executionPayloadEnvelope.d.ts.map +1 -1
  20. package/lib/chain/errors/executionPayloadEnvelope.js +1 -0
  21. package/lib/chain/errors/executionPayloadEnvelope.js.map +1 -1
  22. package/lib/chain/validation/executionPayloadEnvelope.d.ts +6 -0
  23. package/lib/chain/validation/executionPayloadEnvelope.d.ts.map +1 -1
  24. package/lib/chain/validation/executionPayloadEnvelope.js +53 -0
  25. package/lib/chain/validation/executionPayloadEnvelope.js.map +1 -1
  26. package/lib/execution/builder/apiClient.d.ts +14 -8
  27. package/lib/execution/builder/apiClient.d.ts.map +1 -1
  28. package/lib/execution/builder/apiClient.js +12 -10
  29. package/lib/execution/builder/apiClient.js.map +1 -1
  30. package/lib/execution/builder/http.d.ts +2 -0
  31. package/lib/execution/builder/http.d.ts.map +1 -1
  32. package/lib/execution/builder/http.js +2 -0
  33. package/lib/execution/builder/http.js.map +1 -1
  34. package/lib/execution/engine/utils.d.ts.map +1 -1
  35. package/lib/execution/engine/utils.js +3 -2
  36. package/lib/execution/engine/utils.js.map +1 -1
  37. package/lib/metrics/metrics/lodestar.d.ts +1 -0
  38. package/lib/metrics/metrics/lodestar.d.ts.map +1 -1
  39. package/lib/metrics/metrics/lodestar.js +5 -0
  40. package/lib/metrics/metrics/lodestar.js.map +1 -1
  41. package/lib/metrics/server/http.js +5 -5
  42. package/lib/metrics/server/http.js.map +1 -1
  43. package/lib/network/gossip/gossipsub.d.ts +1 -0
  44. package/lib/network/gossip/gossipsub.d.ts.map +1 -1
  45. package/lib/network/gossip/gossipsub.js +10 -0
  46. package/lib/network/gossip/gossipsub.js.map +1 -1
  47. package/lib/network/peers/peerManager.d.ts.map +1 -1
  48. package/lib/network/peers/peerManager.js +28 -1
  49. package/lib/network/peers/peerManager.js.map +1 -1
  50. package/lib/network/peers/utils/prioritizePeers.d.ts +4 -1
  51. package/lib/network/peers/utils/prioritizePeers.d.ts.map +1 -1
  52. package/lib/network/peers/utils/prioritizePeers.js +8 -16
  53. package/lib/network/peers/utils/prioritizePeers.js.map +1 -1
  54. package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
  55. package/lib/network/processor/gossipHandlers.js +21 -11
  56. package/lib/network/processor/gossipHandlers.js.map +1 -1
  57. package/lib/node/nodejs.d.ts +2 -0
  58. package/lib/node/nodejs.d.ts.map +1 -1
  59. package/lib/node/nodejs.js +37 -19
  60. package/lib/node/nodejs.js.map +1 -1
  61. package/lib/node/options.d.ts +3 -1
  62. package/lib/node/options.d.ts.map +1 -1
  63. package/lib/node/options.js +4 -2
  64. package/lib/node/options.js.map +1 -1
  65. package/lib/util/sszBytes.d.ts +5 -0
  66. package/lib/util/sszBytes.d.ts.map +1 -1
  67. package/lib/util/sszBytes.js +33 -0
  68. package/lib/util/sszBytes.js.map +1 -1
  69. package/package.json +16 -16
  70. package/src/api/impl/validator/index.ts +43 -29
  71. package/src/chain/blocks/types.ts +4 -1
  72. package/src/chain/blocks/verifyBlock.ts +30 -16
  73. package/src/chain/blocks/verifyBlocksSignatures.ts +7 -5
  74. package/src/chain/bls/interface.ts +2 -0
  75. package/src/chain/bls/multithread/index.ts +9 -5
  76. package/src/chain/errors/executionPayloadEnvelope.ts +8 -0
  77. package/src/chain/validation/executionPayloadEnvelope.ts +68 -1
  78. package/src/execution/builder/apiClient.ts +16 -13
  79. package/src/execution/builder/http.ts +4 -0
  80. package/src/execution/engine/utils.ts +4 -2
  81. package/src/metrics/metrics/lodestar.ts +5 -0
  82. package/src/metrics/server/http.ts +5 -5
  83. package/src/network/gossip/gossipsub.ts +11 -0
  84. package/src/network/peers/peerManager.ts +27 -2
  85. package/src/network/peers/utils/prioritizePeers.ts +14 -22
  86. package/src/network/processor/gossipHandlers.ts +26 -13
  87. package/src/node/nodejs.ts +34 -15
  88. package/src/node/options.ts +5 -1
  89. package/src/util/sszBytes.ts +40 -0
@@ -169,14 +169,16 @@ function getExecutionEngineStateForPayloadStatus(payloadStatus: ExecutionPayload
169
169
  return ExecutionEngineState.SYNCED;
170
170
 
171
171
  case ExecutionPayloadStatus.ELERROR:
172
- case ExecutionPayloadStatus.INVALID:
173
172
  case ExecutionPayloadStatus.SYNCING:
174
- case ExecutionPayloadStatus.INVALID_BLOCK_HASH:
175
173
  return ExecutionEngineState.SYNCING;
176
174
 
177
175
  case ExecutionPayloadStatus.UNAVAILABLE:
178
176
  return ExecutionEngineState.OFFLINE;
179
177
 
178
+ case ExecutionPayloadStatus.INVALID:
179
+ case ExecutionPayloadStatus.INVALID_BLOCK_HASH:
180
+ return ExecutionEngineState.ONLINE;
181
+
180
182
  default:
181
183
  // In case we can't determine the state, we assume it stays in old state
182
184
  // This assumption is better than considering offline, because the offline state may trigger some notifications
@@ -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
  }
@@ -108,6 +108,7 @@ type GossipSubInternal = GossipSub & {
108
108
  score: {score: (peerIdStr: string) => number};
109
109
  direct: Set<string>;
110
110
  topics: Map<string, Set<string>>;
111
+ streamsOutbound: ReadonlyMap<string, unknown>;
111
112
  start: () => Promise<void>;
112
113
  stop: () => Promise<void>;
113
114
  publish: (topic: TopicStr, data: Uint8Array, opts?: PublishOpts) => Promise<PublishResult>;
@@ -248,6 +249,16 @@ export class Eth2Gossipsub {
248
249
  return this.gossipsub.getMeshPeers(topic);
249
250
  }
250
251
 
252
+ getSubscribersWithOutboundStream(topic: GossipTopic): Set<string> {
253
+ const topicStr = stringifyGossipTopic(this.config, topic);
254
+ const peers = new Set<string>();
255
+ for (const peer of this.gossipsub.getSubscribers(topicStr)) {
256
+ const id = peer.toString();
257
+ if (this.gossipsub.streamsOutbound.has(id)) peers.add(id);
258
+ }
259
+ return peers;
260
+ }
261
+
251
262
  publish(topic: TopicStr, data: Uint8Array, opts?: PublishOpts): Promise<PublishResult> {
252
263
  return this.gossipsub.publish(topic, data, opts);
253
264
  }
@@ -14,6 +14,7 @@ import {NetworkCoreMetrics} from "../core/metrics.js";
14
14
  import {LodestarDiscv5Opts} from "../discv5/types.js";
15
15
  import {INetworkEventBus, NetworkEvent, NetworkEventData} from "../events.js";
16
16
  import {Eth2Gossipsub} from "../gossip/gossipsub.js";
17
+ import {GossipType} from "../gossip/interface.js";
17
18
  import {Libp2p} from "../interface.js";
18
19
  import {SubnetType} from "../metadata.js";
19
20
  import {NetworkConfig} from "../networkConfig.js";
@@ -577,6 +578,8 @@ export class PeerManager {
577
578
  this.lastStatus = status;
578
579
  this.metrics?.peerManager.starved.set(starved ? 1 : 0);
579
580
  const forkSeq = this.config.getForkSeq(this.clock.currentSlot);
581
+ const boundary = this.config.getForkBoundaryAtEpoch(this.clock.currentEpoch);
582
+ const connectedHealthyPeerIds = new Set(connectedHealthyPeers.map((peer) => peer.toString()));
580
583
 
581
584
  const prioritizeTimer = this.metrics?.peerManager.prioritizePeersDuration.startTimer();
582
585
  const {peersToDisconnect, peersToConnect, attnetQueries, syncnetQueries, custodyGroupQueries} = prioritizePeers(
@@ -594,8 +597,30 @@ export class PeerManager {
594
597
  };
595
598
  }),
596
599
  // Collect subnets which we need peers for in the current slot
597
- this.attnetsService.getActiveSubnets(),
598
- this.syncnetsService.getActiveSubnets(),
600
+ this.attnetsService.getActiveSubnets().map((request) => {
601
+ const subscribers = this.gossipsub.getSubscribersWithOutboundStream({
602
+ type: GossipType.beacon_attestation,
603
+ boundary,
604
+ subnet: request.subnet,
605
+ });
606
+ let peerCount = 0;
607
+ for (const peer of subscribers) {
608
+ if (connectedHealthyPeerIds.has(peer)) peerCount++;
609
+ }
610
+ return {...request, peerCount};
611
+ }),
612
+ this.syncnetsService.getActiveSubnets().map((request) => {
613
+ const subscribers = this.gossipsub.getSubscribersWithOutboundStream({
614
+ type: GossipType.sync_committee,
615
+ boundary,
616
+ subnet: request.subnet,
617
+ });
618
+ let peerCount = 0;
619
+ for (const peer of subscribers) {
620
+ if (connectedHealthyPeerIds.has(peer)) peerCount++;
621
+ }
622
+ return {...request, peerCount};
623
+ }),
599
624
  // ignore samplingGroups for pre-fulu forks
600
625
  forkSeq >= ForkSeq.fulu ? this.networkConfig.custodyConfig.sampleGroups : undefined,
601
626
  {
@@ -49,6 +49,8 @@ const syncnetsZero = BitArray.fromBitLen(SYNC_COMMITTEE_SUBNET_COUNT);
49
49
 
50
50
  type SubnetDiscvQuery = {subnet: SubnetID; toSlot: number; maxPeersToDiscover: number};
51
51
 
52
+ export type RequestedSubnetWithPeerCount = RequestedSubnet & {peerCount: number};
53
+
52
54
  /**
53
55
  * A map of das custody group index to maxPeersToDiscover
54
56
  */
@@ -145,8 +147,8 @@ export function prioritizePeers(
145
147
  samplingGroups: CustodyIndex[] | null;
146
148
  score: number;
147
149
  }[],
148
- activeAttnets: RequestedSubnet[],
149
- activeSyncnets: RequestedSubnet[],
150
+ activeAttnets: RequestedSubnetWithPeerCount[],
151
+ activeSyncnets: RequestedSubnetWithPeerCount[],
150
152
  samplingGroups: CustodyIndex[] | undefined,
151
153
  opts: PrioritizePeersOpts,
152
154
  config: ChainConfig,
@@ -231,8 +233,8 @@ export function prioritizePeers(
231
233
  */
232
234
  function requestSubnetPeers(
233
235
  connectedPeers: PeerInfo[],
234
- activeAttnets: RequestedSubnet[],
235
- activeSyncnets: RequestedSubnet[],
236
+ activeAttnets: RequestedSubnetWithPeerCount[],
237
+ activeSyncnets: RequestedSubnetWithPeerCount[],
236
238
  ourSamplingGroups: CustodyIndex[] | undefined,
237
239
  opts: PrioritizePeersOpts,
238
240
  config: ChainConfig,
@@ -252,54 +254,44 @@ function requestSubnetPeers(
252
254
 
253
255
  // attnets, do we need queries for more peers
254
256
  if (activeAttnets.length > 0) {
255
- /** Map of peers per subnet, peer may be in multiple arrays */
256
- const peersPerSubnet = new Map<number, number>();
257
-
258
257
  for (const peer of connectedPeers) {
259
258
  const trueBitIndices = peer.attnetsTrueBitIndices;
260
259
  let dutyCount = 0;
261
260
  for (const {subnet} of activeAttnets) {
262
261
  if (trueBitIndices.includes(subnet)) {
263
262
  dutyCount += 1;
264
- peersPerSubnet.set(subnet, 1 + (peersPerSubnet.get(subnet) ?? 0));
265
263
  }
266
264
  }
267
265
  dutiesByPeer.set(peer, dutyCount);
268
266
  }
269
267
 
270
- for (const {subnet, toSlot} of activeAttnets) {
271
- const peersInSubnet = peersPerSubnet.get(subnet) ?? 0;
272
- metrics?.peerManager.peersPerActiveSubnet.observe({type: SubnetType.attnets}, peersInSubnet);
273
- if (peersInSubnet < targetSubnetPeers) {
268
+ for (const {subnet, toSlot, peerCount} of activeAttnets) {
269
+ metrics?.peerManager.peersPerActiveSubnet.observe({type: SubnetType.attnets}, peerCount);
270
+ if (peerCount < targetSubnetPeers) {
274
271
  // We need more peers
275
- attnetQueries.push({subnet, toSlot, maxPeersToDiscover: targetSubnetPeers - peersInSubnet});
272
+ attnetQueries.push({subnet, toSlot, maxPeersToDiscover: targetSubnetPeers - peerCount});
276
273
  }
277
274
  }
278
275
  }
279
276
 
280
277
  // syncnets, do we need queries for more peers
281
278
  if (activeSyncnets.length > 0) {
282
- /** Map of peers per subnet, peer may be in multiple arrays */
283
- const peersPerSubnet = new Map<number, number>();
284
-
285
279
  for (const peer of connectedPeers) {
286
280
  const trueBitIndices = peer.syncnetsTrueBitIndices;
287
281
  let dutyCount = dutiesByPeer.get(peer) ?? 0;
288
282
  for (const {subnet} of activeSyncnets) {
289
283
  if (trueBitIndices.includes(subnet)) {
290
284
  dutyCount += 1;
291
- peersPerSubnet.set(subnet, 1 + (peersPerSubnet.get(subnet) ?? 0));
292
285
  }
293
286
  }
294
287
  dutiesByPeer.set(peer, dutyCount);
295
288
  }
296
289
 
297
- for (const {subnet, toSlot} of activeSyncnets) {
298
- const peersInSubnet = peersPerSubnet.get(subnet) ?? 0;
299
- metrics?.peerManager.peersPerActiveSubnet.observe({type: SubnetType.syncnets}, peersInSubnet);
300
- if (peersInSubnet < targetSubnetPeers) {
290
+ for (const {subnet, toSlot, peerCount} of activeSyncnets) {
291
+ metrics?.peerManager.peersPerActiveSubnet.observe({type: SubnetType.syncnets}, peerCount);
292
+ if (peerCount < targetSubnetPeers) {
301
293
  // We need more peers
302
- syncnetQueries.push({subnet, toSlot, maxPeersToDiscover: targetSubnetPeers - peersInSubnet});
294
+ syncnetQueries.push({subnet, toSlot, maxPeersToDiscover: targetSubnetPeers - peerCount});
303
295
  }
304
296
  }
305
297
  }
@@ -66,7 +66,10 @@ import {
66
66
  validateGossipGloasDataColumnSidecar,
67
67
  } from "../../chain/validation/dataColumnSidecar.js";
68
68
  import {validateGossipExecutionPayloadBid} from "../../chain/validation/executionPayloadBid.js";
69
- import {validateGossipExecutionPayloadEnvelope} from "../../chain/validation/executionPayloadEnvelope.js";
69
+ import {
70
+ validateGossipExecutionPayloadEnvelope,
71
+ validateGossipExecutionPayloadEnvelopeSerialized,
72
+ } from "../../chain/validation/executionPayloadEnvelope.js";
70
73
  import {InclusionListValidationResult, validateGossipInclusionList} from "../../chain/validation/inclusionList.js";
71
74
  import {
72
75
  AggregateAndProofValidationResult,
@@ -1276,6 +1279,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
1276
1279
  seenTimestampSec,
1277
1280
  }: GossipHandlerParamGeneric<GossipType.execution_payload>) => {
1278
1281
  const {serializedData} = gossipData;
1282
+ validateGossipExecutionPayloadEnvelopeSerialized(chain, serializedData);
1279
1283
  const signedEnvelope = sszDeserialize(topic, serializedData);
1280
1284
  const envelope = signedEnvelope.message;
1281
1285
 
@@ -1328,18 +1332,22 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
1328
1332
  });
1329
1333
  }
1330
1334
 
1335
+ // Add the envelope in the same tick as validation, otherwise a by-root download that completes
1336
+ // before the next event loop sees no envelope, validates it again and adds it first
1337
+ if (!payloadInput.hasPayloadEnvelope()) {
1338
+ chain.serializedCache.set(signedEnvelope, serializedData);
1339
+
1340
+ payloadInput.addPayloadEnvelope({
1341
+ envelope: signedEnvelope,
1342
+ source: PayloadEnvelopeInputSource.gossip,
1343
+ seenTimestampSec,
1344
+ peerIdStr,
1345
+ });
1346
+ }
1347
+
1331
1348
  // Handler - deferred to next event loop so the validation result propagates first
1332
1349
  callInNextEventLoop(() => {
1333
1350
  try {
1334
- chain.serializedCache.set(signedEnvelope, serializedData);
1335
-
1336
- payloadInput.addPayloadEnvelope({
1337
- envelope: signedEnvelope,
1338
- source: PayloadEnvelopeInputSource.gossip,
1339
- seenTimestampSec,
1340
- peerIdStr,
1341
- });
1342
-
1343
1351
  scheduleIncompletePayloadEnvelope(payloadInput, peerIdStr);
1344
1352
 
1345
1353
  chain.emitter.emit(routes.events.EventType.executionPayloadGossip, {
@@ -1349,7 +1357,9 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
1349
1357
  blockRoot: blockRootHex,
1350
1358
  });
1351
1359
 
1352
- chain.processExecutionPayload(payloadInput, {validSignature: true}).catch((e) => {
1360
+ // Signature is verified above only for this envelope, one added by another source is verified at import
1361
+ const validSignature = payloadInput.getPayloadEnvelope() === signedEnvelope;
1362
+ chain.processExecutionPayload(payloadInput, {validSignature}).catch((e) => {
1353
1363
  // Adjust verbosity based on error type
1354
1364
  let logLevel: LogLevel;
1355
1365
 
@@ -1367,11 +1377,14 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
1367
1377
  // Errors might indicate an issue with our node or the connected EL client
1368
1378
  logLevel = LogLevel.error;
1369
1379
  break;
1370
- // INVALID_SIGNATURE should not happen, signature is verified during gossip validation
1380
+ // INVALID_SIGNATURE only happens for an envelope added by another source
1371
1381
  case PayloadErrorCode.INVALID_SIGNATURE:
1372
1382
  case PayloadErrorCode.ENVELOPE_VERIFICATION_ERROR:
1373
1383
  case PayloadErrorCode.EXECUTION_ENGINE_INVALID:
1374
- core.reportPeer(peerIdStr, PeerAction.LowToleranceError, "BadGossipPayload");
1384
+ // Penalize only when the stored envelope is the one this peer sent, not one added by another source
1385
+ if (validSignature) {
1386
+ core.reportPeer(peerIdStr, PeerAction.LowToleranceError, "BadGossipPayload");
1387
+ }
1375
1388
  // The builder may have signed another envelope that is valid, keeping this one would make
1376
1389
  // by-root and range sync reuse it and never import the payload
1377
1390
  chain.seenPayloadEnvelopeInputCache.removeInvalid(payloadInput);
@@ -20,7 +20,7 @@ import {
20
20
  isStatePostGloas,
21
21
  } from "@lodestar/state-transition";
22
22
  import {Slot} from "@lodestar/types";
23
- import {sleep, toRootHex} from "@lodestar/utils";
23
+ import {defer, sleep, toRootHex} from "@lodestar/utils";
24
24
  import {ProcessShutdownCallback} from "@lodestar/validator";
25
25
  import {BeaconRestApiServer, getApi} from "../api/index.js";
26
26
  import {getDataColumnSidecarsMinEpoch} from "../chain/archiveStore/utils/archiveBlocks.js";
@@ -117,6 +117,7 @@ export class BeaconNode {
117
117
 
118
118
  status: BeaconNodeStatus;
119
119
  private controller?: AbortController;
120
+ private closePromise: Promise<void> | undefined;
120
121
 
121
122
  constructor({
122
123
  opts,
@@ -297,6 +298,7 @@ export class BeaconNode {
297
298
  : undefined,
298
299
  builderApiClientOpts: {
299
300
  timeout: opts.executionBuilder.timeout,
301
+ bidDeadline: opts.executionBuilder.bidDeadline,
300
302
  // Sent with all builder api requests, unless the node runs in private mode
301
303
  userAgent: opts.executionBuilder.userAgent,
302
304
  },
@@ -392,23 +394,40 @@ export class BeaconNode {
392
394
  /**
393
395
  * Stop beacon node and its sub-components.
394
396
  */
395
- async close(): Promise<void> {
396
- if (this.status === BeaconNodeStatus.started) {
397
- this.status = BeaconNodeStatus.closing;
398
- this.sync.close();
399
- if (this.restApi) await this.restApi.close();
400
- await this.network.close();
401
- if (this.metricsServer) await this.metricsServer.close();
402
- if (this.monitoring) await this.monitoring.close();
403
- await this.chain.persistToDisk();
404
- await this.chain.close();
397
+ close(): Promise<void> {
398
+ if (this.closePromise !== undefined) return this.closePromise;
399
+ const completion = defer<void>();
400
+ this.closePromise = completion.promise;
401
+ this.status = BeaconNodeStatus.closing;
402
+ void this.closeInternal().then(completion.resolve, completion.reject);
403
+ return this.closePromise;
404
+ }
405
+
406
+ private async closeInternal(): Promise<void> {
407
+ const errors: unknown[] = [];
408
+ for (const cleanup of [
409
+ () => this.sync.close(),
410
+ () => this.restApi?.close(),
411
+ () => this.network.close(),
412
+ () => this.metricsServer?.close(),
413
+ () => this.monitoring?.close(),
414
+ () => this.chain.persistToDisk(),
415
+ () => this.chain.close(),
405
416
  // Abort signal last: close() calls above clear intervals/timeouts so no new
406
417
  // operations get scheduled. If we aborted first, a still-pending interval could
407
418
  // fire and schedule a new operation after abort, leaving it stuck and delaying shutdown.
408
- if (this.controller) this.controller.abort();
409
- await sleep(DELAY_BEFORE_CLOSING_DB_MS);
410
- await this.db.close();
411
- this.status = BeaconNodeStatus.closed;
419
+ () => this.controller?.abort(),
420
+ () => sleep(DELAY_BEFORE_CLOSING_DB_MS),
421
+ () => this.db.close(),
422
+ ]) {
423
+ try {
424
+ await cleanup();
425
+ } catch (error) {
426
+ errors.push(error);
427
+ }
412
428
  }
429
+ this.status = BeaconNodeStatus.closed;
430
+ if (errors.length === 1) throw errors[0];
431
+ if (errors.length > 1) throw new AggregateError(errors, "Beacon node cleanup failed");
413
432
  }
414
433
  }
@@ -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,
@@ -652,6 +652,46 @@ export function getBeaconBlockRootFromExecutionPayloadEnvelopeSerialized(data: U
652
652
  return "0x" + blockRootBuf.toString("hex");
653
653
  }
654
654
 
655
+ // transactions offset within ExecutionPayload fixed portion, see SLOT_NUMBER_OFFSET_IN_EXECUTION_PAYLOAD:
656
+ // everything up to and including blockHash(32) = 504, followed by withdrawals_offset(4)
657
+ const PAYLOAD_START_IN_SIGNED_EXECUTION_PAYLOAD_ENVELOPE =
658
+ ENVELOPE_START_IN_SIGNED + EXECUTION_PAYLOAD_ENVELOPE_FIXED_SIZE; // 180
659
+ const TRANSACTIONS_OFFSET_POSITION_IN_SIGNED_EXECUTION_PAYLOAD_ENVELOPE =
660
+ PAYLOAD_START_IN_SIGNED_EXECUTION_PAYLOAD_ENVELOPE + 504; // 684
661
+ const WITHDRAWALS_OFFSET_POSITION_IN_SIGNED_EXECUTION_PAYLOAD_ENVELOPE =
662
+ TRANSACTIONS_OFFSET_POSITION_IN_SIGNED_EXECUTION_PAYLOAD_ENVELOPE + 4; // 688
663
+
664
+ /**
665
+ * Transaction count of the payload, read from the SSZ offsets without deserializing the envelope.
666
+ * Returns null on malformed offsets, deserialization reports the exact error.
667
+ */
668
+ export function getTransactionCountFromExecutionPayloadEnvelopeSerialized(data: Uint8Array): number | null {
669
+ if (data.length < WITHDRAWALS_OFFSET_POSITION_IN_SIGNED_EXECUTION_PAYLOAD_ENVELOPE + 4) {
670
+ return null;
671
+ }
672
+
673
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
674
+ const transactionsOffset = view.getUint32(TRANSACTIONS_OFFSET_POSITION_IN_SIGNED_EXECUTION_PAYLOAD_ENVELOPE, true);
675
+ const withdrawalsOffset = view.getUint32(WITHDRAWALS_OFFSET_POSITION_IN_SIGNED_EXECUTION_PAYLOAD_ENVELOPE, true);
676
+ if (transactionsOffset > withdrawalsOffset) {
677
+ return null;
678
+ }
679
+ if (transactionsOffset === withdrawalsOffset) {
680
+ return 0;
681
+ }
682
+
683
+ const transactionsStart = PAYLOAD_START_IN_SIGNED_EXECUTION_PAYLOAD_ENVELOPE + transactionsOffset;
684
+ if (transactionsStart + 4 > data.length) {
685
+ return null;
686
+ }
687
+ // first offset of a non-empty list is the size of its offset table, 4 bytes per element
688
+ const firstTransactionOffset = view.getUint32(transactionsStart, true);
689
+ if (firstTransactionOffset % 4 !== 0 || firstTransactionOffset > withdrawalsOffset - transactionsOffset) {
690
+ return null;
691
+ }
692
+ return firstTransactionOffset / 4;
693
+ }
694
+
655
695
  /**
656
696
  * BeaconState of all forks (up until Electra, check with new forks)
657
697
  * class BeaconState(Container):