@lodestar/beacon-node 1.49.0-dev.a18e2b6354 → 1.49.0-dev.ae6cc426f1

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 (37) hide show
  1. package/lib/metrics/metrics/lodestar.d.ts +15 -4
  2. package/lib/metrics/metrics/lodestar.d.ts.map +1 -1
  3. package/lib/metrics/metrics/lodestar.js +28 -1
  4. package/lib/metrics/metrics/lodestar.js.map +1 -1
  5. package/lib/network/gossip/encoding.d.ts +1 -2
  6. package/lib/network/gossip/encoding.d.ts.map +1 -1
  7. package/lib/network/gossip/encoding.js +5 -10
  8. package/lib/network/gossip/encoding.js.map +1 -1
  9. package/lib/network/gossip/gossipsub.js +1 -1
  10. package/lib/network/gossip/gossipsub.js.map +1 -1
  11. package/lib/network/gossip/topic.d.ts +9 -6
  12. package/lib/network/gossip/topic.d.ts.map +1 -1
  13. package/lib/network/gossip/topic.js +23 -8
  14. package/lib/network/gossip/topic.js.map +1 -1
  15. package/lib/network/reqresp/protocols.d.ts.map +1 -1
  16. package/lib/network/reqresp/protocols.js +19 -5
  17. package/lib/network/reqresp/protocols.js.map +1 -1
  18. package/lib/sync/types.d.ts +18 -0
  19. package/lib/sync/types.d.ts.map +1 -1
  20. package/lib/sync/types.js +20 -0
  21. package/lib/sync/types.js.map +1 -1
  22. package/lib/sync/unknownBlock.d.ts.map +1 -1
  23. package/lib/sync/unknownBlock.js +33 -12
  24. package/lib/sync/unknownBlock.js.map +1 -1
  25. package/lib/util/sszBytes.d.ts +5 -0
  26. package/lib/util/sszBytes.d.ts.map +1 -1
  27. package/lib/util/sszBytes.js +13 -1
  28. package/lib/util/sszBytes.js.map +1 -1
  29. package/package.json +14 -14
  30. package/src/metrics/metrics/lodestar.ts +30 -3
  31. package/src/network/gossip/encoding.ts +5 -8
  32. package/src/network/gossip/gossipsub.ts +1 -1
  33. package/src/network/gossip/topic.ts +25 -7
  34. package/src/network/reqresp/protocols.ts +31 -6
  35. package/src/sync/types.ts +20 -0
  36. package/src/sync/unknownBlock.ts +47 -12
  37. package/src/util/sszBytes.ts +18 -0
@@ -1,5 +1,5 @@
1
1
  import {type CompositeTypeAny} from "@chainsafe/ssz";
2
- import {ForkDigestContext} from "@lodestar/config";
2
+ import {BeaconConfig, ChainForkConfig, ForkDigestContext} from "@lodestar/config";
3
3
  import {
4
4
  ATTESTATION_SUBNET_COUNT,
5
5
  ForkName,
@@ -10,32 +10,47 @@ import {
10
10
  isForkPostFulu,
11
11
  isForkPostGloas,
12
12
  } from "@lodestar/params";
13
+ import {TypeSizes} from "@lodestar/reqresp";
13
14
  import {Attestation, SingleAttestation, ssz, sszTypesFor} from "@lodestar/types";
14
15
  import {GossipAction, GossipActionError, GossipErrorCode} from "../../chain/errors/gossipValidation.js";
16
+ import {computeMaxGloasDataColumnSidecarSize} from "../../util/sszBytes.js";
15
17
  import {NetworkConfig} from "../networkConfig.js";
16
18
  import {DEFAULT_ENCODING} from "./constants.js";
17
19
  import {GossipEncoding, GossipTopic, GossipTopicTypeMap, GossipType, SSZTypeOfGossipTopic} from "./interface.js";
18
20
 
19
21
  export interface IGossipTopicCache {
20
22
  getTopic(topicStr: string): GossipTopic;
23
+ getTypeSizes(topicStr: string): TypeSizes;
21
24
  }
22
25
 
23
26
  export class GossipTopicCache implements IGossipTopicCache {
24
27
  private topicsByTopicStr = new Map<string, Required<GossipTopic>>();
28
+ private typeSizesByTopicStr = new Map<string, TypeSizes>();
25
29
 
26
- constructor(private readonly forkDigestContext: ForkDigestContext) {}
30
+ constructor(private readonly config: BeaconConfig) {}
27
31
 
28
32
  /** Returns cached GossipTopic, otherwise attempts to parse it from the str */
29
33
  getTopic(topicStr: string): GossipTopic {
30
34
  let topic = this.topicsByTopicStr.get(topicStr);
31
35
  if (topic === undefined) {
32
- topic = parseGossipTopic(this.forkDigestContext, topicStr);
36
+ topic = parseGossipTopic(this.config, topicStr);
33
37
  // TODO: Consider just throwing here. We should only receive messages from known subscribed topics
34
38
  this.topicsByTopicStr.set(topicStr, topic);
35
39
  }
36
40
  return topic;
37
41
  }
38
42
 
43
+ getTypeSizes(topicStr: string): TypeSizes {
44
+ let typeSizes = this.typeSizesByTopicStr.get(topicStr);
45
+ if (typeSizes === undefined) {
46
+ const topic = this.getTopic(topicStr);
47
+ const sszType = getGossipSSZType(topic);
48
+ typeSizes = {minSize: sszType.minSize, maxSize: getGossipSSZMaxSize(topic, this.config, sszType)};
49
+ this.typeSizesByTopicStr.set(topicStr, typeSizes);
50
+ }
51
+ return typeSizes;
52
+ }
53
+
39
54
  /** Returns cached GossipTopic, otherwise returns undefined */
40
55
  getKnownTopic(topicStr: string): GossipTopic | undefined {
41
56
  return this.topicsByTopicStr.get(topicStr);
@@ -133,11 +148,14 @@ export function getGossipSSZType(topic: GossipTopic) {
133
148
  }
134
149
 
135
150
  /**
136
- * Return the maximum uncompressed SSZ byte length accepted for a gossip object, the SSZ type max size
137
- * or MAX_PAYLOAD_SIZE, whichever is smaller.
151
+ * Return the maximum uncompressed SSZ byte length allowed by the type and configured network bounds.
138
152
  */
139
- export function getGossipSSZMaxSize(topic: GossipTopic, maxPayloadSize: number, sszType?: CompositeTypeAny): number {
140
- return Math.min((sszType ?? getGossipSSZType(topic)).maxSize, maxPayloadSize);
153
+ export function getGossipSSZMaxSize(topic: GossipTopic, config: ChainForkConfig, sszType?: CompositeTypeAny): number {
154
+ const maxSize = Math.min((sszType ?? getGossipSSZType(topic)).maxSize, config.MAX_PAYLOAD_SIZE);
155
+ if (isForkPostGloas(topic.boundary.fork) && topic.type === GossipType.data_column_sidecar) {
156
+ return Math.min(maxSize, computeMaxGloasDataColumnSidecarSize(config));
157
+ }
158
+ return maxSize;
141
159
  }
142
160
 
143
161
  /**
@@ -1,6 +1,7 @@
1
1
  import {BeaconConfig} from "@lodestar/config";
2
- import {ForkName} from "@lodestar/params";
2
+ import {ForkName, isForkPostGloas} from "@lodestar/params";
3
3
  import {ContextBytesFactory, ContextBytesType, Encoding, TypeSizes} from "@lodestar/reqresp";
4
+ import {computeMaxGloasDataColumnSidecarSize} from "../../util/sszBytes.js";
4
5
  import {rateLimitQuotas} from "./rateLimit.js";
5
6
  import {ProtocolNoHandler, ReqRespMethod, Version, requestSszTypeByMethod, responseSszTypeByMethod} from "./types.js";
6
7
 
@@ -151,18 +152,42 @@ function toProtocol(protocol: ProtocolSummary) {
151
152
  encoding: Encoding.SSZ_SNAPPY,
152
153
  contextBytes: toContextBytes(protocol.contextBytesType, config),
153
154
  inboundRateLimits: rateLimitQuotas(fork, config)[protocol.method],
154
- requestSizes: requestType === null ? null : clampTypeSizes(requestType, config),
155
- responseSizes: (fork) => clampTypeSizes(responseSszTypeByMethod[protocol.method](fork, protocol.version), config),
155
+ requestSizes: requestType === null ? null : clampTypeSizes(requestType, config.MAX_PAYLOAD_SIZE),
156
+ responseSizes: (fork) =>
157
+ clampResponseTypeSizes(
158
+ responseSszTypeByMethod[protocol.method](fork, protocol.version),
159
+ protocol.method,
160
+ fork,
161
+ config
162
+ ),
156
163
  };
157
164
  };
158
165
  }
159
166
 
167
+ function clampResponseTypeSizes(
168
+ type: TypeSizes,
169
+ method: ReqRespMethod,
170
+ fork: ForkName,
171
+ config: BeaconConfig
172
+ ): TypeSizes {
173
+ let maxPayloadSize = config.MAX_PAYLOAD_SIZE;
174
+ if (isForkPostGloas(fork)) {
175
+ switch (method) {
176
+ case ReqRespMethod.DataColumnSidecarsByRange:
177
+ case ReqRespMethod.DataColumnSidecarsByRoot:
178
+ maxPayloadSize = Math.min(maxPayloadSize, computeMaxGloasDataColumnSidecarSize(config));
179
+ break;
180
+ }
181
+ }
182
+ return clampTypeSizes(type, maxPayloadSize);
183
+ }
184
+
160
185
  /**
161
- * Length-prefix must be within the SSZ type bounds or MAX_PAYLOAD_SIZE, whichever is smaller
186
+ * Length-prefix must be within the SSZ type bounds or the configured payload limit, whichever is smaller.
162
187
  * https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.12/specs/phase0/p2p-interface.md#encoding-strategies
163
188
  */
164
- function clampTypeSizes(type: TypeSizes, config: BeaconConfig): TypeSizes {
165
- return {minSize: type.minSize, maxSize: Math.min(type.maxSize, config.MAX_PAYLOAD_SIZE)};
189
+ function clampTypeSizes(type: TypeSizes, maxPayloadSize: number): TypeSizes {
190
+ return {minSize: type.minSize, maxSize: Math.min(type.maxSize, maxPayloadSize)};
166
191
  }
167
192
 
168
193
  function toContextBytes(type: ContextBytesType, config: BeaconConfig): ContextBytesFactory {
package/src/sync/types.ts CHANGED
@@ -29,6 +29,26 @@ export enum PendingBlockType {
29
29
  INCOMPLETE_PAYLOAD_ENVELOPE = "incomplete_payload_envelope",
30
30
  }
31
31
 
32
+ export enum DownloadResult {
33
+ /** payload fully downloaded, ready to process */
34
+ Resolved = "resolved",
35
+ /** payload fully downloaded, but it was already imported (via gossip) while we were downloading */
36
+ Late = "late",
37
+ /** envelope downloaded, held until the block lands in fork choice */
38
+ WaitingForBlock = "waiting_for_block",
39
+ /** fetch failed, the pending entry is retained for retry */
40
+ Failed = "failed",
41
+ }
42
+
43
+ export enum FetchResult {
44
+ SuccessResolved = "success_resolved",
45
+ SuccessMissingParent = "success_missing_parent",
46
+ SuccessLate = "success_late",
47
+ SuccessWaitingForBlock = "success_waiting_for_block",
48
+ FailureTriedAllPeers = "failure_tried_all_peers",
49
+ FailureMaxAttempts = "failure_max_attempts",
50
+ }
51
+
32
52
  export enum PendingBlockInputStatus {
33
53
  pending = "pending",
34
54
  fetching = "fetching",
@@ -26,7 +26,9 @@ import {MAX_CONCURRENT_REQUESTS} from "./constants.js";
26
26
  import {SyncOptions} from "./options.js";
27
27
  import {
28
28
  BlockInputSyncCacheItem,
29
+ DownloadResult,
29
30
  DroppedItemReason,
31
+ FetchResult,
30
32
  PayloadSyncCacheItem,
31
33
  PendingBlockInput,
32
34
  PendingBlockInputStatus,
@@ -63,14 +65,6 @@ type AdvancePendingBlockResult =
63
65
  | "blocked"
64
66
  | "removed";
65
67
 
66
- enum FetchResult {
67
- SuccessResolved = "success_resolved",
68
- SuccessMissingParent = "success_missing_parent",
69
- SuccessLate = "success_late",
70
- FailureTriedAllPeers = "failure_tried_all_peers",
71
- FailureMaxAttempts = "failure_max_attempts",
72
- }
73
-
74
68
  class UnknownBlockRateLimitedError extends Error {
75
69
  constructor(message: string) {
76
70
  super(message);
@@ -1125,15 +1119,25 @@ export class BlockInputSync {
1125
1119
  return;
1126
1120
  }
1127
1121
  this.pendingPayloads.set(resultRootHex, pendingPayload);
1122
+ const result = isPendingPayloadEnvelope(pendingPayload)
1123
+ ? DownloadResult.WaitingForBlock
1124
+ : this.chain.forkChoice.hasPayloadHexUnsafe(resultRootHex)
1125
+ ? DownloadResult.Late
1126
+ : DownloadResult.Resolved;
1127
+ this.metrics?.blockInputSync.downloadedPayloadsResult.inc({result});
1128
1128
 
1129
1129
  if (isPendingPayloadEnvelope(pendingPayload)) {
1130
1130
  await this.reconcilePayloadEnvelope(pendingPayload);
1131
1131
  } else if (pendingPayload.status === PendingPayloadInputStatus.downloaded) {
1132
+ this.metrics?.blockInputSync.elapsedTimeTillPayloadReceived.observe(
1133
+ Date.now() / 1000 - computeTimeAtSlot(this.config, pendingPayload.payloadInput.slot, this.chain.genesisTime)
1134
+ );
1132
1135
  await this.processPayload(pendingPayload);
1133
1136
  }
1134
1137
  return;
1135
1138
  }
1136
1139
 
1140
+ this.metrics?.blockInputSync.downloadedPayloadsResult.inc({result: DownloadResult.Failed});
1137
1141
  this.logger.debug("Ignoring unknown payload root after failed download", logCtx, res.err);
1138
1142
  if (!isPendingPayloadEnvelope(payload)) {
1139
1143
  payload.status = PendingPayloadInputStatus.pending;
@@ -1248,6 +1252,11 @@ export class BlockInputSync {
1248
1252
  : await this.chain.seenPayloadEnvelopeInputCache.getOrReload(rootHex);
1249
1253
  let envelope = payloadInput?.hasPayloadEnvelope() ? payloadInput.getPayloadEnvelope() : undefined;
1250
1254
 
1255
+ const fetchStartSec = Date.now() / 1000;
1256
+ if (typeof slot === "number") {
1257
+ this.metrics?.blockInputSync.payloadFetchBegin.observe(this.chain.clock.secFromSlot(slot, fetchStartSec));
1258
+ }
1259
+
1251
1260
  let i = 0;
1252
1261
  let deferredByRateLimit = false;
1253
1262
  while (i++ < this.getMaxDownloadAttempts()) {
@@ -1271,6 +1280,11 @@ export class BlockInputSync {
1271
1280
  pendingColumns.size > 0
1272
1281
  ? `cannot find peer with needed columns=${prettyPrintIndices(Array.from(pendingColumns))}`
1273
1282
  : "no peer available to download pending payload";
1283
+ this.metrics?.blockInputSync.payloadFetchTime.observe(
1284
+ {result: FetchResult.FailureTriedAllPeers},
1285
+ Date.now() / 1000 - fetchStartSec
1286
+ );
1287
+ this.metrics?.blockInputSync.payloadFetchPeers.set({result: FetchResult.FailureTriedAllPeers}, i);
1274
1288
  throw Error(`Error fetching payload by root slot=${slot} root=${rootHex} after ${i}: ${reason}`);
1275
1289
  }
1276
1290
 
@@ -1280,7 +1294,12 @@ export class BlockInputSync {
1280
1294
  try {
1281
1295
  if (!envelope) {
1282
1296
  envelope = await this.fetchExecutionPayloadEnvelope(peerId, blockRoot, rootHex);
1297
+ const slotWasUnknown = typeof slot !== "number";
1283
1298
  slot = envelope.message.payload.slotNumber;
1299
+ if (slotWasUnknown) {
1300
+ // we were not able to observe the time into slot when starting the fetch, do it now
1301
+ this.metrics?.blockInputSync.payloadFetchBegin.observe(this.chain.clock.secFromSlot(slot, fetchStartSec));
1302
+ }
1284
1303
  }
1285
1304
 
1286
1305
  payloadInput ??= await this.chain.seenPayloadEnvelopeInputCache.getOrReload(rootHex);
@@ -1289,6 +1308,11 @@ export class BlockInputSync {
1289
1308
  // envelope and wait for the block body; reconcilePayloadEnvelope validates once the block lands.
1290
1309
  // payloadInput may be seeded from the block body during download, so a non-null payloadInput does not
1291
1310
  // imply the block is imported.
1311
+ this.metrics?.blockInputSync.payloadFetchTime.observe(
1312
+ {result: FetchResult.SuccessWaitingForBlock},
1313
+ Date.now() / 1000 - fetchStartSec
1314
+ );
1315
+ this.metrics?.blockInputSync.payloadFetchPeers.set({result: FetchResult.SuccessWaitingForBlock}, i);
1292
1316
  return {
1293
1317
  status: PendingPayloadInputStatus.waitingForBlock,
1294
1318
  envelope,
@@ -1339,6 +1363,12 @@ export class BlockInputSync {
1339
1363
  });
1340
1364
 
1341
1365
  if (pendingPayload.status === PendingPayloadInputStatus.downloaded) {
1366
+ // late = the payload got imported (via gossip) while we were downloading, same as the block path
1367
+ const result = this.chain.forkChoice.hasPayloadHexUnsafe(rootHex)
1368
+ ? FetchResult.SuccessLate
1369
+ : FetchResult.SuccessResolved;
1370
+ this.metrics?.blockInputSync.payloadFetchTime.observe({result}, Date.now() / 1000 - fetchStartSec);
1371
+ this.metrics?.blockInputSync.payloadFetchPeers.set({result}, i);
1342
1372
  return pendingPayload;
1343
1373
  }
1344
1374
 
@@ -1373,12 +1403,17 @@ export class BlockInputSync {
1373
1403
  }
1374
1404
  }
1375
1405
 
1406
+ this.metrics?.blockInputSync.payloadFetchTime.observe(
1407
+ {result: FetchResult.FailureMaxAttempts},
1408
+ Date.now() / 1000 - fetchStartSec
1409
+ );
1410
+ this.metrics?.blockInputSync.payloadFetchPeers.set({result: FetchResult.FailureMaxAttempts}, i - 1);
1411
+
1376
1412
  if (deferredByRateLimit && this.peerBalancer.getNextRateLimitRetryAt() !== null) {
1377
1413
  throw new UnknownBlockRateLimitedError(
1378
1414
  `Error fetching payload with slot=${slot} root=${rootHex} after ${i - 1} attempts: peers are rate-limited`
1379
1415
  );
1380
1416
  }
1381
-
1382
1417
  throw Error(`Error fetching payload with slot=${slot} root=${rootHex} after ${i - 1} attempts.`);
1383
1418
  }
1384
1419
 
@@ -1483,7 +1518,7 @@ export class BlockInputSync {
1483
1518
 
1484
1519
  // no more peer with needed columns to try, throw error
1485
1520
  const message = `Error fetching UnknownBlockRoot slot=${slot} root=${rootHex} after ${i}: cannot find peer with needed columns=${prettyPrintIndices(Array.from(pendingColumns))}`;
1486
- this.metrics?.blockInputSync.fetchTimeSec.observe(
1521
+ this.metrics?.blockInputSync.fetchTime.observe(
1487
1522
  {result: FetchResult.FailureTriedAllPeers},
1488
1523
  Date.now() / 1000 - fetchStartSec
1489
1524
  );
@@ -1572,7 +1607,7 @@ export class BlockInputSync {
1572
1607
  : this.chain.forkChoice.hasBlockHex(cacheItem.blockInput.parentRootHex)
1573
1608
  ? FetchResult.SuccessResolved
1574
1609
  : FetchResult.SuccessMissingParent;
1575
- this.metrics?.blockInputSync.fetchTimeSec.observe({result}, Date.now() / 1000 - fetchStartSec);
1610
+ this.metrics?.blockInputSync.fetchTime.observe({result}, Date.now() / 1000 - fetchStartSec);
1576
1611
  this.metrics?.blockInputSync.fetchPeers.set({result}, i);
1577
1612
  return cacheItem;
1578
1613
  }
@@ -1606,7 +1641,7 @@ export class BlockInputSync {
1606
1641
  }
1607
1642
  }
1608
1643
 
1609
- this.metrics?.blockInputSync.fetchTimeSec.observe(
1644
+ this.metrics?.blockInputSync.fetchTime.observe(
1610
1645
  {result: FetchResult.FailureMaxAttempts},
1611
1646
  Date.now() / 1000 - fetchStartSec
1612
1647
  );
@@ -1,6 +1,7 @@
1
1
  import {BitArray, deserializeUint8ArrayBitListFromBytes} from "@chainsafe/ssz";
2
2
  import {ChainForkConfig} from "@lodestar/config";
3
3
  import {
4
+ BYTES_PER_CELL,
4
5
  BYTES_PER_FIELD_ELEMENT,
5
6
  FIELD_ELEMENTS_PER_BLOB,
6
7
  ForkName,
@@ -551,6 +552,23 @@ export function getBeaconBlockRootFromDataColumnSidecarSerialized(data: Uint8Arr
551
552
  return "0x" + blockRootBuf.toString("hex");
552
553
  }
553
554
 
555
+ // beaconBlockRoot is the last fixed field in the Gloas layout
556
+ const DATA_COLUMN_SIDECAR_GLOAS_FIXED_SIZE = BEACON_BLOCK_ROOT_POSITION_IN_GLOAS_DATA_COLUMN_SIDECAR + ROOT_SIZE; // 56 bytes
557
+ const KZG_PROOF_SIZE = 48; // Bytes48
558
+ const DATA_COLUMN_SIDECAR_GLOAS_PER_BLOB_SIZE = BYTES_PER_CELL + KZG_PROOF_SIZE; // 2048 + 48 = 2096 bytes
559
+
560
+ /**
561
+ * Gloas sidecar size bound uses the largest blob limit across the entire schedule, including future entries.
562
+ * https://github.com/ethereum/consensus-specs/pull/5613
563
+ */
564
+ export function computeMaxGloasDataColumnSidecarSize(config: ChainForkConfig): number {
565
+ const maxBlobs = config.BLOB_SCHEDULE.reduce(
566
+ (max, entry) => Math.max(max, entry.MAX_BLOBS_PER_BLOCK),
567
+ config.MAX_BLOBS_PER_BLOCK_ELECTRA
568
+ );
569
+ return DATA_COLUMN_SIDECAR_GLOAS_FIXED_SIZE + maxBlobs * DATA_COLUMN_SIDECAR_GLOAS_PER_BLOB_SIZE;
570
+ }
571
+
554
572
  /**
555
573
  * SignedExecutionPayloadEnvelope SSZ Layout:
556
574
  * ├─ 4 bytes: message offset (points to byte 100)