@lodestar/beacon-node 1.49.0-dev.4d35c8b82e → 1.49.0-dev.57ffc39074

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 (128) hide show
  1. package/lib/api/impl/beacon/blocks/index.js +2 -2
  2. package/lib/api/impl/beacon/blocks/index.js.map +1 -1
  3. package/lib/api/impl/validator/index.d.ts.map +1 -1
  4. package/lib/api/impl/validator/index.js +9 -7
  5. package/lib/api/impl/validator/index.js.map +1 -1
  6. package/lib/chain/archiveStore/historicalState/metrics.d.ts.map +1 -1
  7. package/lib/chain/archiveStore/historicalState/metrics.js +4 -0
  8. package/lib/chain/archiveStore/historicalState/metrics.js.map +1 -1
  9. package/lib/chain/archiveStore/utils/pruneHistory.d.ts.map +1 -1
  10. package/lib/chain/archiveStore/utils/pruneHistory.js +14 -3
  11. package/lib/chain/archiveStore/utils/pruneHistory.js.map +1 -1
  12. package/lib/chain/blocks/index.js +2 -2
  13. package/lib/chain/blocks/index.js.map +1 -1
  14. package/lib/chain/blocks/payloadEnvelopeInput/types.d.ts +1 -1
  15. package/lib/chain/blocks/payloadEnvelopeInput/types.d.ts.map +1 -1
  16. package/lib/chain/emitter.d.ts +10 -0
  17. package/lib/chain/emitter.d.ts.map +1 -1
  18. package/lib/chain/emitter.js +4 -0
  19. package/lib/chain/emitter.js.map +1 -1
  20. package/lib/chain/errors/attesterSlashingError.d.ts +4 -1
  21. package/lib/chain/errors/attesterSlashingError.d.ts.map +1 -1
  22. package/lib/chain/errors/attesterSlashingError.js +1 -0
  23. package/lib/chain/errors/attesterSlashingError.js.map +1 -1
  24. package/lib/chain/errors/payloadAttestation.d.ts +4 -0
  25. package/lib/chain/errors/payloadAttestation.d.ts.map +1 -1
  26. package/lib/chain/errors/payloadAttestation.js +1 -0
  27. package/lib/chain/errors/payloadAttestation.js.map +1 -1
  28. package/lib/chain/errors/proposerSlashingError.d.ts +4 -1
  29. package/lib/chain/errors/proposerSlashingError.d.ts.map +1 -1
  30. package/lib/chain/errors/proposerSlashingError.js +1 -0
  31. package/lib/chain/errors/proposerSlashingError.js.map +1 -1
  32. package/lib/chain/options.d.ts +0 -4
  33. package/lib/chain/options.d.ts.map +1 -1
  34. package/lib/chain/options.js +0 -1
  35. package/lib/chain/options.js.map +1 -1
  36. package/lib/chain/produceBlock/produceBlockBody.d.ts.map +1 -1
  37. package/lib/chain/produceBlock/produceBlockBody.js +6 -6
  38. package/lib/chain/produceBlock/produceBlockBody.js.map +1 -1
  39. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts +2 -1
  40. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts.map +1 -1
  41. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js +3 -2
  42. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js.map +1 -1
  43. package/lib/chain/validation/attesterSlashing.d.ts.map +1 -1
  44. package/lib/chain/validation/attesterSlashing.js +1 -2
  45. package/lib/chain/validation/attesterSlashing.js.map +1 -1
  46. package/lib/chain/validation/payloadAttestationMessage.d.ts.map +1 -1
  47. package/lib/chain/validation/payloadAttestationMessage.js +8 -1
  48. package/lib/chain/validation/payloadAttestationMessage.js.map +1 -1
  49. package/lib/chain/validation/proposerSlashing.js +1 -2
  50. package/lib/chain/validation/proposerSlashing.js.map +1 -1
  51. package/lib/chain/validatorMonitor.d.ts.map +1 -1
  52. package/lib/chain/validatorMonitor.js +2 -2
  53. package/lib/chain/validatorMonitor.js.map +1 -1
  54. package/lib/db/repositories/blockArchive.d.ts +5 -0
  55. package/lib/db/repositories/blockArchive.d.ts.map +1 -1
  56. package/lib/db/repositories/blockArchive.js +36 -1
  57. package/lib/db/repositories/blockArchive.js.map +1 -1
  58. package/lib/metrics/metrics/lodestar.d.ts +19 -4
  59. package/lib/metrics/metrics/lodestar.d.ts.map +1 -1
  60. package/lib/metrics/metrics/lodestar.js +38 -1
  61. package/lib/metrics/metrics/lodestar.js.map +1 -1
  62. package/lib/network/gossip/encoding.d.ts +1 -2
  63. package/lib/network/gossip/encoding.d.ts.map +1 -1
  64. package/lib/network/gossip/encoding.js +5 -10
  65. package/lib/network/gossip/encoding.js.map +1 -1
  66. package/lib/network/gossip/gossipsub.d.ts.map +1 -1
  67. package/lib/network/gossip/gossipsub.js +16 -1
  68. package/lib/network/gossip/gossipsub.js.map +1 -1
  69. package/lib/network/gossip/topic.d.ts +9 -6
  70. package/lib/network/gossip/topic.d.ts.map +1 -1
  71. package/lib/network/gossip/topic.js +23 -8
  72. package/lib/network/gossip/topic.js.map +1 -1
  73. package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
  74. package/lib/network/processor/gossipHandlers.js +16 -5
  75. package/lib/network/processor/gossipHandlers.js.map +1 -1
  76. package/lib/network/processor/gossipValidatorFn.d.ts +2 -0
  77. package/lib/network/processor/gossipValidatorFn.d.ts.map +1 -1
  78. package/lib/network/processor/gossipValidatorFn.js +88 -7
  79. package/lib/network/processor/gossipValidatorFn.js.map +1 -1
  80. package/lib/network/processor/index.d.ts +6 -2
  81. package/lib/network/processor/index.d.ts.map +1 -1
  82. package/lib/network/processor/index.js +62 -24
  83. package/lib/network/processor/index.js.map +1 -1
  84. package/lib/network/reqresp/protocols.d.ts.map +1 -1
  85. package/lib/network/reqresp/protocols.js +19 -5
  86. package/lib/network/reqresp/protocols.js.map +1 -1
  87. package/lib/sync/types.d.ts +39 -0
  88. package/lib/sync/types.d.ts.map +1 -1
  89. package/lib/sync/types.js +42 -0
  90. package/lib/sync/types.js.map +1 -1
  91. package/lib/sync/unknownBlock.d.ts +16 -0
  92. package/lib/sync/unknownBlock.d.ts.map +1 -1
  93. package/lib/sync/unknownBlock.js +186 -13
  94. package/lib/sync/unknownBlock.js.map +1 -1
  95. package/lib/util/sszBytes.d.ts +5 -0
  96. package/lib/util/sszBytes.d.ts.map +1 -1
  97. package/lib/util/sszBytes.js +13 -1
  98. package/lib/util/sszBytes.js.map +1 -1
  99. package/package.json +14 -14
  100. package/src/api/impl/beacon/blocks/index.ts +2 -2
  101. package/src/api/impl/validator/index.ts +9 -7
  102. package/src/chain/archiveStore/historicalState/metrics.ts +4 -0
  103. package/src/chain/archiveStore/utils/pruneHistory.ts +14 -4
  104. package/src/chain/blocks/index.ts +2 -2
  105. package/src/chain/blocks/payloadEnvelopeInput/types.ts +1 -1
  106. package/src/chain/emitter.ts +7 -2
  107. package/src/chain/errors/attesterSlashingError.ts +3 -1
  108. package/src/chain/errors/payloadAttestation.ts +2 -0
  109. package/src/chain/errors/proposerSlashingError.ts +3 -1
  110. package/src/chain/options.ts +0 -5
  111. package/src/chain/produceBlock/produceBlockBody.ts +17 -6
  112. package/src/chain/seenCache/seenPayloadEnvelopeInput.ts +3 -2
  113. package/src/chain/validation/attesterSlashing.ts +1 -2
  114. package/src/chain/validation/payloadAttestationMessage.ts +9 -0
  115. package/src/chain/validation/proposerSlashing.ts +1 -2
  116. package/src/chain/validatorMonitor.ts +12 -2
  117. package/src/db/repositories/blockArchive.ts +38 -1
  118. package/src/metrics/metrics/lodestar.ts +46 -3
  119. package/src/network/gossip/encoding.ts +5 -8
  120. package/src/network/gossip/gossipsub.ts +19 -2
  121. package/src/network/gossip/topic.ts +25 -7
  122. package/src/network/processor/gossipHandlers.ts +18 -5
  123. package/src/network/processor/gossipValidatorFn.ts +111 -7
  124. package/src/network/processor/index.ts +81 -24
  125. package/src/network/reqresp/protocols.ts +31 -6
  126. package/src/sync/types.ts +44 -3
  127. package/src/sync/unknownBlock.ts +209 -13
  128. package/src/util/sszBytes.ts +18 -0
@@ -5,7 +5,7 @@ import {ForkSeq, SLOTS_PER_EPOCH, isForkPostGloas} from "@lodestar/params";
5
5
  import {RequestError, RequestErrorCode} from "@lodestar/reqresp";
6
6
  import {computeTimeAtSlot} from "@lodestar/state-transition";
7
7
  import {RootHex, Slot, gloas} from "@lodestar/types";
8
- import {Logger, fromHex, prettyPrintIndices, pruneSetToMax, sleep, toRootHex} from "@lodestar/utils";
8
+ import {Logger, fromHex, isErrorAborted, prettyPrintIndices, pruneSetToMax, sleep, toRootHex} from "@lodestar/utils";
9
9
  import {isBlockInputBlobs, isBlockInputColumns} from "../chain/blocks/blockInput/blockInput.js";
10
10
  import {BlockInputSource, IBlockInput} from "../chain/blocks/blockInput/types.js";
11
11
  import {PayloadError, PayloadErrorCode} from "../chain/blocks/importExecutionPayload.js";
@@ -18,6 +18,7 @@ import {Metrics} from "../metrics/index.js";
18
18
  import {INetwork, NetworkEvent, NetworkEventData, prettyPrintPeerIdStr} from "../network/index.js";
19
19
  import {PeerSyncMeta} from "../network/peers/peersData.js";
20
20
  import {MAX_PEERS_PER_ROOT} from "../network/processor/constants.js";
21
+ import {ClockEvent} from "../util/clock.js";
21
22
  import {PeerIdStr} from "../util/peerId.js";
22
23
  import {shuffle} from "../util/shuffle.js";
23
24
  import {sortBy} from "../util/sortBy.js";
@@ -26,7 +27,10 @@ import {MAX_CONCURRENT_REQUESTS} from "./constants.js";
26
27
  import {SyncOptions} from "./options.js";
27
28
  import {
28
29
  BlockInputSyncCacheItem,
30
+ DeferredPayloadResult,
31
+ DownloadResult,
29
32
  DroppedItemReason,
33
+ FetchResult,
30
34
  PayloadSyncCacheItem,
31
35
  PendingBlockInput,
32
36
  PendingBlockInputStatus,
@@ -55,6 +59,11 @@ const MAX_PENDING_BLOCKS = 100;
55
59
  */
56
60
  const PRUNE_UNRESOLVED_SLOT_EPOCHS = 2;
57
61
 
62
+ /**
63
+ * Poll pending payloads of the current slot this often, between PAYLOAD_DUE and the end of the slot.
64
+ */
65
+ export const PAYLOAD_POLL_INTERVAL_MS = 1_000;
66
+
58
67
  type AdvancePendingBlockResult =
59
68
  | "ready"
60
69
  | "queued_block"
@@ -63,14 +72,6 @@ type AdvancePendingBlockResult =
63
72
  | "blocked"
64
73
  | "removed";
65
74
 
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
75
  class UnknownBlockRateLimitedError extends Error {
75
76
  constructor(message: string) {
76
77
  super(message);
@@ -122,6 +123,11 @@ export class BlockInputSync {
122
123
  private subscribedToNetworkEvents = false;
123
124
  private peerBalancer: UnknownBlockPeerBalancer;
124
125
  private rateLimitBackoffTimeout: NodeJS.Timeout | undefined;
126
+ /**
127
+ * Search unknown payload starting at PAYLOAD_DUE_BPS.
128
+ */
129
+ private readonly deferredPayloadRoots = new Map<RootHex, {slot: Slot; source: BlockInputSource; peer?: PeerIdStr}>();
130
+ private payloadPollAbortController: AbortController | undefined;
125
131
 
126
132
  constructor(
127
133
  private readonly config: ChainForkConfig,
@@ -158,6 +164,7 @@ export class BlockInputSync {
158
164
  this.logger.verbose("BlockInputSync enabled.");
159
165
  this.chain.emitter.on(ChainEvent.unknownBlockRoot, this.onUnknownBlockRoot);
160
166
  this.chain.emitter.on(ChainEvent.unknownEnvelopeBlockRoot, this.onUnknownEnvelopeBlockRoot);
167
+ this.chain.emitter.on(ChainEvent.unknownEnvelopeBlockRootSlot, this.onUnknownEnvelopeBlockRootSlot);
161
168
  this.chain.emitter.on(ChainEvent.incompleteBlockInput, this.onIncompleteBlockInput);
162
169
  this.chain.emitter.on(ChainEvent.incompletePayloadEnvelope, this.onIncompletePayloadEnvelope);
163
170
  this.chain.emitter.on(ChainEvent.blockUnknownParent, this.onUnknownParent);
@@ -166,6 +173,7 @@ export class BlockInputSync {
166
173
  this.chain.emitter.on(routes.events.EventType.executionPayload, this.onPayloadImported);
167
174
  this.network.events.on(NetworkEvent.peerConnected, this.onPeerConnected);
168
175
  this.network.events.on(NetworkEvent.peerDisconnected, this.onPeerDisconnected);
176
+ this.chain.clock.on(ClockEvent.slot, this.onClockSlot);
169
177
 
170
178
  // Seed the balancer with peers that connected before we subscribed
171
179
  for (const peerId of this.network.getConnectedPeers()) {
@@ -177,14 +185,22 @@ export class BlockInputSync {
177
185
  }
178
186
 
179
187
  this.subscribedToNetworkEvents = true;
188
+ // we subscribe mid-slot, and a root can be deferred at any point in the slot, so start this
189
+ // slot's loop now instead of waiting for the next ClockEvent.slot
190
+ this.onClockSlot(this.chain.clock.currentSlot);
180
191
  }
181
192
  }
182
193
 
183
194
  unsubscribeFromNetwork(): void {
184
195
  this.logger.verbose("BlockInputSync disabled.");
185
196
  this.clearRateLimitBackoffTimer();
197
+ this.chain.clock.off(ClockEvent.slot, this.onClockSlot);
198
+ this.payloadPollAbortController?.abort();
199
+ this.payloadPollAbortController = undefined;
200
+ this.deferredPayloadRoots.clear();
186
201
  this.chain.emitter.off(ChainEvent.unknownBlockRoot, this.onUnknownBlockRoot);
187
202
  this.chain.emitter.off(ChainEvent.unknownEnvelopeBlockRoot, this.onUnknownEnvelopeBlockRoot);
203
+ this.chain.emitter.off(ChainEvent.unknownEnvelopeBlockRootSlot, this.onUnknownEnvelopeBlockRootSlot);
188
204
  this.chain.emitter.off(ChainEvent.incompleteBlockInput, this.onIncompleteBlockInput);
189
205
  this.chain.emitter.off(ChainEvent.incompletePayloadEnvelope, this.onIncompletePayloadEnvelope);
190
206
  this.chain.emitter.off(ChainEvent.blockUnknownParent, this.onUnknownParent);
@@ -248,6 +264,33 @@ export class BlockInputSync {
248
264
  }
249
265
  };
250
266
 
267
+ private onUnknownEnvelopeBlockRootSlot = (data: ChainEventData[ChainEvent.unknownEnvelopeBlockRootSlot]): void => {
268
+ try {
269
+ if (data.slot === this.chain.clock.currentSlot) {
270
+ // we receive the optimistic search, the poll loop searches it at PAYLOAD_DUE or on its next
271
+ // tick. If that loop already returned - every root it watched was imported - this root waits
272
+ // for the next slot, where onClockSlot moves it to pendingPayloads. That is not the normal
273
+ // scenario, and in a forking condition it only delays the search of the extra root
274
+ const deferred = this.deferredPayloadRoots.get(data.rootHex);
275
+ if (deferred === undefined) {
276
+ this.deferredPayloadRoots.set(data.rootHex, {slot: data.slot, source: data.source, peer: data.peer});
277
+ } else if (deferred.peer === undefined) {
278
+ deferred.peer = data.peer;
279
+ }
280
+ return;
281
+ }
282
+
283
+ const isNewRoot = this.addByPayloadRootHex(data.rootHex, data.peer, data.slot);
284
+ if (isNewRoot) {
285
+ this.triggerUnknownBlockSearch();
286
+ this.metrics?.blockInputSync.requests.inc({type: PendingBlockType.UNKNOWN_PAYLOAD_BLOCK_ROOT_SLOT});
287
+ this.metrics?.blockInputSync.payloadSource.inc({source: data.source});
288
+ }
289
+ } catch (e) {
290
+ this.logger.debug("Error handling unknownEnvelopeBlockRootSlot event", {}, e as Error);
291
+ }
292
+ };
293
+
251
294
  private onIncompletePayloadEnvelope = (data: ChainEventData[ChainEvent.incompletePayloadEnvelope]): void => {
252
295
  try {
253
296
  this.addByPayloadInput(data.payloadInput, data.peer);
@@ -791,6 +834,118 @@ export class BlockInputSync {
791
834
  }
792
835
  }
793
836
 
837
+ private onClockSlot = (slot: Slot): void => {
838
+ if (!isForkPostGloas(this.config.getForkName(slot))) {
839
+ return;
840
+ }
841
+ // end the previous slot's loop, wherever it is: its sleep is bound to this same controller
842
+ this.payloadPollAbortController?.abort();
843
+ this.payloadPollAbortController = new AbortController();
844
+ // entries from an earlier slot is not polled
845
+ for (const [rootHex, {slot: deferredSlot, source, peer}] of this.deferredPayloadRoots) {
846
+ if (deferredSlot < slot) {
847
+ if (!this.chain.forkChoice.hasPayloadHexUnsafe(rootHex)) {
848
+ this.addDeferredPayloadRoot(rootHex, deferredSlot, source, peer);
849
+ }
850
+ this.deferredPayloadRoots.delete(rootHex);
851
+ }
852
+ }
853
+ this.pollPayloadsAtSlot(slot, this.payloadPollAbortController.signal).catch((e) => {
854
+ if (!isErrorAborted(e)) {
855
+ this.logger.debug("Error polling payloads", {slot}, e as Error);
856
+ }
857
+ });
858
+ };
859
+
860
+ /**
861
+ * Poll unknown payloads of the current slot.
862
+ */
863
+ private async pollPayloadsAtSlot(slot: Slot, signal: AbortSignal): Promise<void> {
864
+ // fixed tick count so the loop cannot outlive its slot: for mainnet, it's 6 ticks
865
+ const tickCount = Math.floor(
866
+ (this.config.SLOT_DURATION_MS - this.config.getPayloadDueMs()) / PAYLOAD_POLL_INTERVAL_MS
867
+ );
868
+ // a root still missing on several ticks is searched on each of them, recorded once
869
+ const searchedRoots = new Set<RootHex>();
870
+ let polls = 0;
871
+
872
+ try {
873
+ await sleep(Math.max(0, this.config.getPayloadDueMs() - this.chain.clock.msFromSlot(slot)), signal);
874
+
875
+ for (polls = 0; polls < tickCount; polls++) {
876
+ let importedCount = 0;
877
+ for (const [rootHex, {slot: deferredSlot, source, peer}] of this.deferredPayloadRoots) {
878
+ if (this.chain.forkChoice.hasPayloadHexUnsafe(rootHex)) {
879
+ importedCount++;
880
+ continue;
881
+ }
882
+ // copy to the regular pendingPayloads so that we can search
883
+ this.addDeferredPayloadRoot(rootHex, deferredSlot, source, peer);
884
+ searchedRoots.add(rootHex);
885
+ }
886
+
887
+ // an empty deferredPayloadRoots means there is no optimistic search comes before PAYLOAD_DUE
888
+ // we have to poll until the payload of current slot is imported or slot end
889
+ if (this.deferredPayloadRoots.size > 0 && importedCount === this.deferredPayloadRoots.size) {
890
+ if (searchedRoots.size > 0) {
891
+ // the backup search was needed
892
+ this.metrics?.blockInputSync.deferredPayloadResult.inc({
893
+ result: DeferredPayloadResult.ImportedAfterSearch,
894
+ });
895
+ this.metrics?.blockInputSync.deferredPayloadPolls.observe(polls);
896
+ this.logger.verbose("Payload imported after unknown sync search", {
897
+ slot,
898
+ roots: Array.from(searchedRoots).join(","),
899
+ polls,
900
+ });
901
+ } else {
902
+ // this happens most of the time on devnet
903
+ // gossip delivered everything before PAYLOAD_DUE
904
+ this.metrics?.blockInputSync.deferredPayloadResult.inc({
905
+ result: DeferredPayloadResult.ImportedBeforeSearch,
906
+ });
907
+ }
908
+ return;
909
+ }
910
+
911
+ this.triggerUnknownBlockSearch();
912
+ if (polls < tickCount - 1) {
913
+ // no sleep after the last tick: the loop would otherwise end right at the slot boundary,
914
+ // where onClockSlot aborts it
915
+ await sleep(PAYLOAD_POLL_INTERVAL_MS, signal);
916
+ }
917
+ }
918
+
919
+ // reaching here means the early return never fired
920
+ if (this.deferredPayloadRoots.size > 0) {
921
+ this.metrics?.blockInputSync.deferredPayloadResult.inc({result: DeferredPayloadResult.Unresolved});
922
+ this.logger.debug("Deferred payloads still missing at end of slot", {
923
+ slot,
924
+ roots: Array.from(this.deferredPayloadRoots.keys()).join(","),
925
+ });
926
+ } else {
927
+ // no optimistic search and no payload import ended the loop, eg a skipped slot
928
+ this.metrics?.blockInputSync.deferredPayloadResult.inc({result: DeferredPayloadResult.ResolvedNoRoot});
929
+ this.logger.debug("No deferred payloads at slot", {
930
+ slot,
931
+ });
932
+ }
933
+ } catch (e) {
934
+ if (!isErrorAborted(e)) {
935
+ throw e;
936
+ }
937
+ this.logger.verbose("Payload poll aborted", {slot, polls, msIntoSlot: this.chain.clock.msFromSlot(slot)});
938
+ }
939
+ }
940
+
941
+ private addDeferredPayloadRoot(rootHex: RootHex, slot: Slot, source: BlockInputSource, peer?: PeerIdStr): void {
942
+ const isNewRoot = this.addByPayloadRootHex(rootHex, peer, slot);
943
+ if (isNewRoot) {
944
+ this.metrics?.blockInputSync.requests.inc({type: PendingBlockType.DEFERRED_PAYLOAD_BLOCK_ROOT});
945
+ this.metrics?.blockInputSync.payloadSource.inc({source});
946
+ }
947
+ }
948
+
794
949
  private async downloadBlock(block: BlockInputSyncCacheItem): Promise<void> {
795
950
  if (block.status !== PendingBlockInputStatus.pending) {
796
951
  return;
@@ -1125,15 +1280,25 @@ export class BlockInputSync {
1125
1280
  return;
1126
1281
  }
1127
1282
  this.pendingPayloads.set(resultRootHex, pendingPayload);
1283
+ const result = isPendingPayloadEnvelope(pendingPayload)
1284
+ ? DownloadResult.WaitingForBlock
1285
+ : this.chain.forkChoice.hasPayloadHexUnsafe(resultRootHex)
1286
+ ? DownloadResult.Late
1287
+ : DownloadResult.Resolved;
1288
+ this.metrics?.blockInputSync.downloadedPayloadsResult.inc({result});
1128
1289
 
1129
1290
  if (isPendingPayloadEnvelope(pendingPayload)) {
1130
1291
  await this.reconcilePayloadEnvelope(pendingPayload);
1131
1292
  } else if (pendingPayload.status === PendingPayloadInputStatus.downloaded) {
1293
+ this.metrics?.blockInputSync.elapsedTimeTillPayloadReceived.observe(
1294
+ Date.now() / 1000 - computeTimeAtSlot(this.config, pendingPayload.payloadInput.slot, this.chain.genesisTime)
1295
+ );
1132
1296
  await this.processPayload(pendingPayload);
1133
1297
  }
1134
1298
  return;
1135
1299
  }
1136
1300
 
1301
+ this.metrics?.blockInputSync.downloadedPayloadsResult.inc({result: DownloadResult.Failed});
1137
1302
  this.logger.debug("Ignoring unknown payload root after failed download", logCtx, res.err);
1138
1303
  if (!isPendingPayloadEnvelope(payload)) {
1139
1304
  payload.status = PendingPayloadInputStatus.pending;
@@ -1248,6 +1413,11 @@ export class BlockInputSync {
1248
1413
  : await this.chain.seenPayloadEnvelopeInputCache.getOrReload(rootHex);
1249
1414
  let envelope = payloadInput?.hasPayloadEnvelope() ? payloadInput.getPayloadEnvelope() : undefined;
1250
1415
 
1416
+ const fetchStartSec = Date.now() / 1000;
1417
+ if (typeof slot === "number") {
1418
+ this.metrics?.blockInputSync.payloadFetchBegin.observe(this.chain.clock.secFromSlot(slot, fetchStartSec));
1419
+ }
1420
+
1251
1421
  let i = 0;
1252
1422
  let deferredByRateLimit = false;
1253
1423
  while (i++ < this.getMaxDownloadAttempts()) {
@@ -1271,6 +1441,11 @@ export class BlockInputSync {
1271
1441
  pendingColumns.size > 0
1272
1442
  ? `cannot find peer with needed columns=${prettyPrintIndices(Array.from(pendingColumns))}`
1273
1443
  : "no peer available to download pending payload";
1444
+ this.metrics?.blockInputSync.payloadFetchTime.observe(
1445
+ {result: FetchResult.FailureTriedAllPeers},
1446
+ Date.now() / 1000 - fetchStartSec
1447
+ );
1448
+ this.metrics?.blockInputSync.payloadFetchPeers.set({result: FetchResult.FailureTriedAllPeers}, i);
1274
1449
  throw Error(`Error fetching payload by root slot=${slot} root=${rootHex} after ${i}: ${reason}`);
1275
1450
  }
1276
1451
 
@@ -1280,7 +1455,12 @@ export class BlockInputSync {
1280
1455
  try {
1281
1456
  if (!envelope) {
1282
1457
  envelope = await this.fetchExecutionPayloadEnvelope(peerId, blockRoot, rootHex);
1458
+ const slotWasUnknown = typeof slot !== "number";
1283
1459
  slot = envelope.message.payload.slotNumber;
1460
+ if (slotWasUnknown) {
1461
+ // we were not able to observe the time into slot when starting the fetch, do it now
1462
+ this.metrics?.blockInputSync.payloadFetchBegin.observe(this.chain.clock.secFromSlot(slot, fetchStartSec));
1463
+ }
1284
1464
  }
1285
1465
 
1286
1466
  payloadInput ??= await this.chain.seenPayloadEnvelopeInputCache.getOrReload(rootHex);
@@ -1289,6 +1469,11 @@ export class BlockInputSync {
1289
1469
  // envelope and wait for the block body; reconcilePayloadEnvelope validates once the block lands.
1290
1470
  // payloadInput may be seeded from the block body during download, so a non-null payloadInput does not
1291
1471
  // imply the block is imported.
1472
+ this.metrics?.blockInputSync.payloadFetchTime.observe(
1473
+ {result: FetchResult.SuccessWaitingForBlock},
1474
+ Date.now() / 1000 - fetchStartSec
1475
+ );
1476
+ this.metrics?.blockInputSync.payloadFetchPeers.set({result: FetchResult.SuccessWaitingForBlock}, i);
1292
1477
  return {
1293
1478
  status: PendingPayloadInputStatus.waitingForBlock,
1294
1479
  envelope,
@@ -1339,6 +1524,12 @@ export class BlockInputSync {
1339
1524
  });
1340
1525
 
1341
1526
  if (pendingPayload.status === PendingPayloadInputStatus.downloaded) {
1527
+ // late = the payload got imported (via gossip) while we were downloading, same as the block path
1528
+ const result = this.chain.forkChoice.hasPayloadHexUnsafe(rootHex)
1529
+ ? FetchResult.SuccessLate
1530
+ : FetchResult.SuccessResolved;
1531
+ this.metrics?.blockInputSync.payloadFetchTime.observe({result}, Date.now() / 1000 - fetchStartSec);
1532
+ this.metrics?.blockInputSync.payloadFetchPeers.set({result}, i);
1342
1533
  return pendingPayload;
1343
1534
  }
1344
1535
 
@@ -1373,12 +1564,17 @@ export class BlockInputSync {
1373
1564
  }
1374
1565
  }
1375
1566
 
1567
+ this.metrics?.blockInputSync.payloadFetchTime.observe(
1568
+ {result: FetchResult.FailureMaxAttempts},
1569
+ Date.now() / 1000 - fetchStartSec
1570
+ );
1571
+ this.metrics?.blockInputSync.payloadFetchPeers.set({result: FetchResult.FailureMaxAttempts}, i - 1);
1572
+
1376
1573
  if (deferredByRateLimit && this.peerBalancer.getNextRateLimitRetryAt() !== null) {
1377
1574
  throw new UnknownBlockRateLimitedError(
1378
1575
  `Error fetching payload with slot=${slot} root=${rootHex} after ${i - 1} attempts: peers are rate-limited`
1379
1576
  );
1380
1577
  }
1381
-
1382
1578
  throw Error(`Error fetching payload with slot=${slot} root=${rootHex} after ${i - 1} attempts.`);
1383
1579
  }
1384
1580
 
@@ -1483,7 +1679,7 @@ export class BlockInputSync {
1483
1679
 
1484
1680
  // no more peer with needed columns to try, throw error
1485
1681
  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(
1682
+ this.metrics?.blockInputSync.fetchTime.observe(
1487
1683
  {result: FetchResult.FailureTriedAllPeers},
1488
1684
  Date.now() / 1000 - fetchStartSec
1489
1685
  );
@@ -1572,7 +1768,7 @@ export class BlockInputSync {
1572
1768
  : this.chain.forkChoice.hasBlockHex(cacheItem.blockInput.parentRootHex)
1573
1769
  ? FetchResult.SuccessResolved
1574
1770
  : FetchResult.SuccessMissingParent;
1575
- this.metrics?.blockInputSync.fetchTimeSec.observe({result}, Date.now() / 1000 - fetchStartSec);
1771
+ this.metrics?.blockInputSync.fetchTime.observe({result}, Date.now() / 1000 - fetchStartSec);
1576
1772
  this.metrics?.blockInputSync.fetchPeers.set({result}, i);
1577
1773
  return cacheItem;
1578
1774
  }
@@ -1606,7 +1802,7 @@ export class BlockInputSync {
1606
1802
  }
1607
1803
  }
1608
1804
 
1609
- this.metrics?.blockInputSync.fetchTimeSec.observe(
1805
+ this.metrics?.blockInputSync.fetchTime.observe(
1610
1806
  {result: FetchResult.FailureMaxAttempts},
1611
1807
  Date.now() / 1000 - fetchStartSec
1612
1808
  );
@@ -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)