@lodestar/beacon-node 1.43.0-rc.1 → 1.43.0-rc.5

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 (97) hide show
  1. package/lib/chain/blocks/utils/chainSegment.d.ts +1 -3
  2. package/lib/chain/blocks/utils/chainSegment.d.ts.map +1 -1
  3. package/lib/chain/blocks/utils/chainSegment.js +29 -26
  4. package/lib/chain/blocks/utils/chainSegment.js.map +1 -1
  5. package/lib/metrics/metrics/lodestar.d.ts +4 -0
  6. package/lib/metrics/metrics/lodestar.d.ts.map +1 -1
  7. package/lib/metrics/metrics/lodestar.js +5 -0
  8. package/lib/metrics/metrics/lodestar.js.map +1 -1
  9. package/lib/network/gossip/topic.d.ts +749 -2
  10. package/lib/network/gossip/topic.d.ts.map +1 -1
  11. package/lib/network/interface.d.ts +1 -0
  12. package/lib/network/interface.d.ts.map +1 -1
  13. package/lib/network/network.d.ts +1 -0
  14. package/lib/network/network.d.ts.map +1 -1
  15. package/lib/network/network.js +3 -0
  16. package/lib/network/network.js.map +1 -1
  17. package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
  18. package/lib/network/processor/gossipHandlers.js +31 -1
  19. package/lib/network/processor/gossipHandlers.js.map +1 -1
  20. package/lib/network/reqresp/ReqRespBeaconNode.d.ts.map +1 -1
  21. package/lib/network/reqresp/ReqRespBeaconNode.js +1 -1
  22. package/lib/network/reqresp/ReqRespBeaconNode.js.map +1 -1
  23. package/lib/network/reqresp/handlers/beaconBlocksByHead.d.ts +9 -0
  24. package/lib/network/reqresp/handlers/beaconBlocksByHead.d.ts.map +1 -0
  25. package/lib/network/reqresp/handlers/beaconBlocksByHead.js +61 -0
  26. package/lib/network/reqresp/handlers/beaconBlocksByHead.js.map +1 -0
  27. package/lib/network/reqresp/handlers/index.d.ts.map +1 -1
  28. package/lib/network/reqresp/handlers/index.js +5 -0
  29. package/lib/network/reqresp/handlers/index.js.map +1 -1
  30. package/lib/network/reqresp/interface.d.ts +1 -1
  31. package/lib/network/reqresp/interface.js +1 -1
  32. package/lib/network/reqresp/protocols.d.ts +1 -0
  33. package/lib/network/reqresp/protocols.d.ts.map +1 -1
  34. package/lib/network/reqresp/protocols.js +5 -0
  35. package/lib/network/reqresp/protocols.js.map +1 -1
  36. package/lib/network/reqresp/rateLimit.d.ts.map +1 -1
  37. package/lib/network/reqresp/rateLimit.js +4 -0
  38. package/lib/network/reqresp/rateLimit.js.map +1 -1
  39. package/lib/network/reqresp/score.d.ts.map +1 -1
  40. package/lib/network/reqresp/score.js +1 -0
  41. package/lib/network/reqresp/score.js.map +1 -1
  42. package/lib/network/reqresp/types.d.ts +3 -0
  43. package/lib/network/reqresp/types.d.ts.map +1 -1
  44. package/lib/network/reqresp/types.js +3 -0
  45. package/lib/network/reqresp/types.js.map +1 -1
  46. package/lib/sync/constants.d.ts +5 -1
  47. package/lib/sync/constants.d.ts.map +1 -1
  48. package/lib/sync/constants.js +5 -1
  49. package/lib/sync/constants.js.map +1 -1
  50. package/lib/sync/range/batch.d.ts +20 -3
  51. package/lib/sync/range/batch.d.ts.map +1 -1
  52. package/lib/sync/range/batch.js +54 -10
  53. package/lib/sync/range/batch.js.map +1 -1
  54. package/lib/sync/range/chain.d.ts +3 -0
  55. package/lib/sync/range/chain.d.ts.map +1 -1
  56. package/lib/sync/range/chain.js +44 -5
  57. package/lib/sync/range/chain.js.map +1 -1
  58. package/lib/sync/range/range.d.ts.map +1 -1
  59. package/lib/sync/range/range.js +40 -2
  60. package/lib/sync/range/range.js.map +1 -1
  61. package/lib/sync/range/utils/peerBalancer.d.ts +2 -1
  62. package/lib/sync/range/utils/peerBalancer.d.ts.map +1 -1
  63. package/lib/sync/range/utils/peerBalancer.js +8 -4
  64. package/lib/sync/range/utils/peerBalancer.js.map +1 -1
  65. package/lib/sync/unknownBlock.d.ts.map +1 -1
  66. package/lib/sync/unknownBlock.js +1 -12
  67. package/lib/sync/unknownBlock.js.map +1 -1
  68. package/lib/sync/utils/downloadByRange.d.ts +7 -1
  69. package/lib/sync/utils/downloadByRange.d.ts.map +1 -1
  70. package/lib/sync/utils/downloadByRange.js +2 -0
  71. package/lib/sync/utils/downloadByRange.js.map +1 -1
  72. package/lib/sync/utils/rateLimit.d.ts +2 -0
  73. package/lib/sync/utils/rateLimit.d.ts.map +1 -0
  74. package/lib/sync/utils/rateLimit.js +15 -0
  75. package/lib/sync/utils/rateLimit.js.map +1 -0
  76. package/package.json +16 -16
  77. package/src/chain/blocks/utils/chainSegment.ts +30 -27
  78. package/src/metrics/metrics/lodestar.ts +6 -0
  79. package/src/network/interface.ts +1 -0
  80. package/src/network/network.ts +12 -0
  81. package/src/network/processor/gossipHandlers.ts +35 -1
  82. package/src/network/reqresp/ReqRespBeaconNode.ts +1 -0
  83. package/src/network/reqresp/handlers/beaconBlocksByHead.ts +91 -0
  84. package/src/network/reqresp/handlers/index.ts +5 -0
  85. package/src/network/reqresp/interface.ts +1 -1
  86. package/src/network/reqresp/protocols.ts +6 -0
  87. package/src/network/reqresp/rateLimit.ts +4 -0
  88. package/src/network/reqresp/score.ts +1 -0
  89. package/src/network/reqresp/types.ts +5 -0
  90. package/src/sync/constants.ts +5 -1
  91. package/src/sync/range/batch.ts +71 -11
  92. package/src/sync/range/chain.ts +52 -10
  93. package/src/sync/range/range.ts +52 -2
  94. package/src/sync/range/utils/peerBalancer.ts +9 -3
  95. package/src/sync/unknownBlock.ts +1 -14
  96. package/src/sync/utils/downloadByRange.ts +8 -0
  97. package/src/sync/utils/rateLimit.ts +16 -0
@@ -3,6 +3,7 @@ import {NotReorgedReason} from "@lodestar/fork-choice";
3
3
  import {ArchiveStoreTask} from "../../chain/archiveStore/archiveStore.js";
4
4
  import {FrequencyStateArchiveStep} from "../../chain/archiveStore/strategies/frequencyStateArchiveStrategy.js";
5
5
  import {BlockInputSource} from "../../chain/blocks/blockInput/index.js";
6
+ import {PayloadErrorCode} from "../../chain/blocks/importExecutionPayload.js";
6
7
  import {PayloadEnvelopeInputSource} from "../../chain/blocks/payloadEnvelopeInput/index.js";
7
8
  import {JobQueueItemType} from "../../chain/bls/index.js";
8
9
  import {AttestationErrorCode, BlockErrorCode} from "../../chain/errors/index.js";
@@ -890,6 +891,11 @@ export function createLodestarMetrics(
890
891
  labelNames: ["source"],
891
892
  buckets: [0.5, 1, 2, 4, 6, 12],
892
893
  }),
894
+ processPayloadErrors: register.gauge<{error: PayloadErrorCode | "NOT_PAYLOAD_ERROR"}>({
895
+ name: "lodestar_gossip_execution_payload_envelope_process_payload_errors",
896
+ help: "Count of errors, by error type, while processing execution payload envelopes",
897
+ labelNames: ["error"],
898
+ }),
893
899
  },
894
900
  // recovery in the case of specific blob rows required
895
901
  recoverBlobSidecars: {
@@ -78,6 +78,7 @@ export interface INetwork extends INetworkCorePublic {
78
78
  // ReqResp
79
79
  sendBeaconBlocksByRange(peerId: PeerIdStr, request: phase0.BeaconBlocksByRangeRequest): Promise<SignedBeaconBlock[]>;
80
80
  sendBeaconBlocksByRoot(peerId: PeerIdStr, request: BeaconBlocksByRootRequest): Promise<SignedBeaconBlock[]>;
81
+ sendBeaconBlocksByHead(peerId: PeerIdStr, request: fulu.BeaconBlocksByHeadRequest): Promise<SignedBeaconBlock[]>;
81
82
  sendBlobSidecarsByRange(peerId: PeerIdStr, request: deneb.BlobSidecarsByRangeRequest): Promise<deneb.BlobSidecar[]>;
82
83
  sendBlobSidecarsByRoot(peerId: PeerIdStr, request: BlobSidecarsByRootRequest): Promise<deneb.BlobSidecar[]>;
83
84
  sendDataColumnSidecarsByRange(
@@ -578,6 +578,18 @@ export class Network implements INetwork {
578
578
  );
579
579
  }
580
580
 
581
+ async sendBeaconBlocksByHead(
582
+ peerId: PeerIdStr,
583
+ request: fulu.BeaconBlocksByHeadRequest
584
+ ): Promise<SignedBeaconBlock[]> {
585
+ return collectMaxResponseTypedWithBytes(
586
+ this.sendReqRespRequest(peerId, ReqRespMethod.BeaconBlocksByHead, [Version.V1], request),
587
+ Math.min(request.count, this.config.MAX_REQUEST_BLOCKS_DENEB),
588
+ responseSszTypeByMethod[ReqRespMethod.BeaconBlocksByHead],
589
+ this.chain.serializedCache
590
+ );
591
+ }
592
+
581
593
  async sendLightClientBootstrap(peerId: PeerIdStr, request: Root): Promise<LightClientBootstrap> {
582
594
  return collectExactOneTyped(
583
595
  this.sendReqRespRequest(peerId, ReqRespMethod.LightClientBootstrap, [Version.V1], request),
@@ -35,6 +35,7 @@ import {
35
35
  IBlockInput,
36
36
  isBlockInputColumns,
37
37
  } from "../../chain/blocks/blockInput/index.js";
38
+ import {PayloadError, PayloadErrorCode} from "../../chain/blocks/importExecutionPayload.js";
38
39
  import {PayloadEnvelopeInput, PayloadEnvelopeInputSource} from "../../chain/blocks/payloadEnvelopeInput/index.js";
39
40
  import {BlobSidecarValidation} from "../../chain/blocks/types.js";
40
41
  import {ChainEvent} from "../../chain/emitter.js";
@@ -1148,7 +1149,40 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
1148
1149
  });
1149
1150
 
1150
1151
  chain.processExecutionPayload(payloadInput, {validSignature: true}).catch((e) => {
1151
- chain.logger.debug("Error processing execution payload from gossip", {slot, root: blockRootHex}, e as Error);
1152
+ // Adjust verbosity based on error type
1153
+ let logLevel: LogLevel;
1154
+
1155
+ if (e instanceof PayloadError) {
1156
+ switch (e.type.code) {
1157
+ // BLOCK_NOT_IN_FORK_CHOICE should not happen, validateGossipExecutionPayloadEnvelope above
1158
+ // already verified the block is in fork choice
1159
+ case PayloadErrorCode.BLOCK_NOT_IN_FORK_CHOICE:
1160
+ case PayloadErrorCode.MISS_BLOCK_STATE:
1161
+ case PayloadErrorCode.EXECUTION_ENGINE_ERROR:
1162
+ // Errors might indicate an issue with our node or the connected EL client
1163
+ logLevel = LogLevel.error;
1164
+ break;
1165
+ // INVALID_SIGNATURE should not happen, signature is verified during gossip validation
1166
+ case PayloadErrorCode.INVALID_SIGNATURE:
1167
+ case PayloadErrorCode.ENVELOPE_VERIFICATION_ERROR:
1168
+ case PayloadErrorCode.EXECUTION_ENGINE_INVALID:
1169
+ core.reportPeer(peerIdStr, PeerAction.LowToleranceError, "BadGossipPayload");
1170
+ // Misbehaving peer, but could highlight an issue in another client
1171
+ logLevel = LogLevel.warn;
1172
+ break;
1173
+ }
1174
+ } else {
1175
+ // Any unexpected error
1176
+ logLevel = LogLevel.error;
1177
+ }
1178
+ metrics?.gossipExecutionPayloadEnvelope.processPayloadErrors.inc({
1179
+ error: e instanceof PayloadError ? e.type.code : "NOT_PAYLOAD_ERROR",
1180
+ });
1181
+ chain.logger[logLevel](
1182
+ "Error processing execution payload from gossip",
1183
+ {slot, peer: peerIdStr, root: blockRootHex},
1184
+ e as Error
1185
+ );
1152
1186
  });
1153
1187
  },
1154
1188
  [GossipType.payload_attestation_message]: async ({
@@ -286,6 +286,7 @@ export class ReqRespBeaconNode extends ReqResp {
286
286
  // instead of protocol version. This is not easily fixable with our current architecture.
287
287
  // See https://github.com/ChainSafe/lodestar/pull/8168 for more details.
288
288
  [protocols.StatusV2(fork, this.config), this.onStatus.bind(this)],
289
+ [protocols.BeaconBlocksByHead(fork, this.config), this.getHandler(ReqRespMethod.BeaconBlocksByHead)],
289
290
  [
290
291
  protocols.DataColumnSidecarsByRoot(fork, this.config),
291
292
  this.getHandler(ReqRespMethod.DataColumnSidecarsByRoot),
@@ -0,0 +1,91 @@
1
+ import {PeerId} from "@libp2p/interface";
2
+ import {BeaconConfig} from "@lodestar/config";
3
+ import {ForkName, GENESIS_EPOCH, GENESIS_SLOT, isForkPostDeneb} from "@lodestar/params";
4
+ import {RespStatus, ResponseError, ResponseOutgoing} from "@lodestar/reqresp";
5
+ import {computeEpochAtSlot, computeStartSlotAtEpoch} from "@lodestar/state-transition";
6
+ import {fulu} from "@lodestar/types";
7
+ import {toRootHex} from "@lodestar/utils";
8
+ import {IBeaconChain} from "../../../chain/index.js";
9
+ import {getParentRootFromSignedBeaconBlockSerialized} from "../../../util/sszBytes.js";
10
+ import {prettyPrintPeerId} from "../../util.js";
11
+
12
+ // See https://github.com/ethereum/consensus-specs/pull/5181
13
+ export async function* onBeaconBlocksByHead(
14
+ request: fulu.BeaconBlocksByHeadRequest,
15
+ chain: IBeaconChain,
16
+ peerId: PeerId,
17
+ peerClient: string
18
+ ): AsyncIterable<ResponseOutgoing> {
19
+ const currentFork = chain.config.getForkName(chain.clock.currentSlot);
20
+ const {beaconRoot, count} = validateBeaconBlocksByHeadRequest(currentFork, chain.config, request);
21
+
22
+ const requestedRootHex = toRootHex(beaconRoot);
23
+ let blockRootHex = requestedRootHex;
24
+ const minimumRequestEpoch = Math.max(
25
+ GENESIS_EPOCH,
26
+ chain.clock.currentEpoch - chain.config.MIN_EPOCHS_FOR_BLOCK_REQUESTS
27
+ );
28
+ const minimumRequestSlot = computeStartSlotAtEpoch(minimumRequestEpoch);
29
+
30
+ for (let blocksSent = 0; blocksSent < count; blocksSent++) {
31
+ const blockBytes = await chain.getSerializedBlockByRoot(blockRootHex);
32
+ if (!blockBytes) {
33
+ if (blocksSent === 0) {
34
+ throw new ResponseError(RespStatus.RESOURCE_UNAVAILABLE, `Unknown block root ${requestedRootHex}`);
35
+ }
36
+ return;
37
+ }
38
+
39
+ if (blockBytes.slot < minimumRequestSlot) {
40
+ if (blocksSent === 0) {
41
+ chain.logger.verbose("Peer requested unavailable block for BeaconBlocksByHead", {
42
+ peer: prettyPrintPeerId(peerId),
43
+ client: peerClient,
44
+ requestedRoot: requestedRootHex,
45
+ slot: blockBytes.slot,
46
+ minimumRequestSlot,
47
+ });
48
+ }
49
+ return;
50
+ }
51
+
52
+ yield {
53
+ data: blockBytes.block,
54
+ boundary: chain.config.getForkBoundaryAtEpoch(computeEpochAtSlot(blockBytes.slot)),
55
+ };
56
+
57
+ if (blockBytes.slot === GENESIS_SLOT) {
58
+ return;
59
+ }
60
+
61
+ const parentRootHex = getParentRootFromSignedBeaconBlockSerialized(blockBytes.block);
62
+ if (parentRootHex === null) {
63
+ throw new ResponseError(
64
+ RespStatus.SERVER_ERROR,
65
+ `Invalid block bytes for root ${blockRootHex} slot ${blockBytes.slot}`
66
+ );
67
+ }
68
+ blockRootHex = parentRootHex;
69
+ }
70
+ }
71
+
72
+ export function validateBeaconBlocksByHeadRequest(
73
+ fork: ForkName,
74
+ config: BeaconConfig,
75
+ request: fulu.BeaconBlocksByHeadRequest
76
+ ): fulu.BeaconBlocksByHeadRequest {
77
+ const {beaconRoot} = request;
78
+ let {count} = request;
79
+
80
+ if (count < 1) {
81
+ throw new ResponseError(RespStatus.INVALID_REQUEST, "count < 1");
82
+ }
83
+
84
+ const maxRequestBlocks = isForkPostDeneb(fork) ? config.MAX_REQUEST_BLOCKS_DENEB : config.MAX_REQUEST_BLOCKS;
85
+
86
+ if (count > maxRequestBlocks) {
87
+ count = maxRequestBlocks;
88
+ }
89
+
90
+ return {beaconRoot, count};
91
+ }
@@ -9,6 +9,7 @@ import {
9
9
  ExecutionPayloadEnvelopesByRootRequestType,
10
10
  } from "../../../util/types.js";
11
11
  import {GetReqRespHandlerFn, ReqRespMethod} from "../types.js";
12
+ import {onBeaconBlocksByHead} from "./beaconBlocksByHead.js";
12
13
  import {onBeaconBlocksByRange} from "./beaconBlocksByRange.js";
13
14
  import {onBeaconBlocksByRoot} from "./beaconBlocksByRoot.js";
14
15
  import {onBlobSidecarsByRange} from "./blobSidecarsByRange.js";
@@ -47,6 +48,10 @@ export function getReqRespHandlers({db, chain}: {db: IBeaconDb; chain: IBeaconCh
47
48
  const body = BeaconBlocksByRootRequestType(fork, chain.config).deserialize(req.data);
48
49
  return onBeaconBlocksByRoot(body, chain);
49
50
  },
51
+ [ReqRespMethod.BeaconBlocksByHead]: (req, peerId, peerClient) => {
52
+ const body = ssz.fulu.BeaconBlocksByHeadRequest.deserialize(req.data);
53
+ return onBeaconBlocksByHead(body, chain, peerId, peerClient);
54
+ },
50
55
  [ReqRespMethod.BlobSidecarsByRoot]: (req) => {
51
56
  const fork = chain.config.getForkName(chain.clock.currentSlot);
52
57
  const body = BlobSidecarsByRootRequestType(fork, chain.config).deserialize(req.data);
@@ -33,7 +33,7 @@ export enum RespStatus {
33
33
  */
34
34
  SERVER_ERROR = 2,
35
35
  /**
36
- * The responder does not have requested resource. The response payload adheres to the ErrorMessage schema (described below). Note: This response code is only valid as a response to BlocksByRange
36
+ * The responder does not have requested resource. The response payload adheres to the ErrorMessage schema.
37
37
  */
38
38
  RESOURCE_UNAVAILABLE = 3,
39
39
  /**
@@ -70,6 +70,12 @@ export const BeaconBlocksByRootV2 = toProtocol({
70
70
  contextBytesType: ContextBytesType.ForkDigest,
71
71
  });
72
72
 
73
+ export const BeaconBlocksByHead = toProtocol({
74
+ method: ReqRespMethod.BeaconBlocksByHead,
75
+ version: Version.V1,
76
+ contextBytesType: ContextBytesType.ForkDigest,
77
+ });
78
+
73
79
  export const BlobSidecarsByRange = toProtocol({
74
80
  method: ReqRespMethod.BlobSidecarsByRange,
75
81
  version: Version.V1,
@@ -40,6 +40,10 @@ export const rateLimitQuotas: (fork: ForkName, config: BeaconConfig) => Record<R
40
40
  },
41
41
  getRequestCount: getRequestCountFn(fork, config, ReqRespMethod.BeaconBlocksByRoot, (req) => req.length),
42
42
  },
43
+ [ReqRespMethod.BeaconBlocksByHead]: {
44
+ byPeer: {quota: config.MAX_REQUEST_BLOCKS_DENEB, quotaTimeMs: 10_000},
45
+ getRequestCount: getRequestCountFn(fork, config, ReqRespMethod.BeaconBlocksByHead, (req) => req.count),
46
+ },
43
47
  [ReqRespMethod.BlobSidecarsByRange]: {
44
48
  // Rationale: MAX_REQUEST_BLOCKS_DENEB * MAX_BLOBS_PER_BLOCK
45
49
  byPeer: {
@@ -46,6 +46,7 @@ export function onOutgoingReqRespError(e: RequestError, method: ReqRespMethod):
46
46
  return PeerAction.LowToleranceError;
47
47
  case ReqRespMethod.BeaconBlocksByRange:
48
48
  case ReqRespMethod.BeaconBlocksByRoot:
49
+ case ReqRespMethod.BeaconBlocksByHead:
49
50
  case ReqRespMethod.ExecutionPayloadEnvelopesByRoot:
50
51
  case ReqRespMethod.ExecutionPayloadEnvelopesByRange:
51
52
  return PeerAction.MidToleranceError;
@@ -42,6 +42,7 @@ export enum ReqRespMethod {
42
42
  Metadata = "metadata",
43
43
  BeaconBlocksByRange = "beacon_blocks_by_range",
44
44
  BeaconBlocksByRoot = "beacon_blocks_by_root",
45
+ BeaconBlocksByHead = "beacon_blocks_by_head",
45
46
  BlobSidecarsByRange = "blob_sidecars_by_range",
46
47
  BlobSidecarsByRoot = "blob_sidecars_by_root",
47
48
  DataColumnSidecarsByRange = "data_column_sidecars_by_range",
@@ -62,6 +63,7 @@ export type RequestBodyByMethod = {
62
63
  [ReqRespMethod.Metadata]: null;
63
64
  [ReqRespMethod.BeaconBlocksByRange]: phase0.BeaconBlocksByRangeRequest;
64
65
  [ReqRespMethod.BeaconBlocksByRoot]: BeaconBlocksByRootRequest;
66
+ [ReqRespMethod.BeaconBlocksByHead]: fulu.BeaconBlocksByHeadRequest;
65
67
  [ReqRespMethod.BlobSidecarsByRange]: deneb.BlobSidecarsByRangeRequest;
66
68
  [ReqRespMethod.BlobSidecarsByRoot]: BlobSidecarsByRootRequest;
67
69
  [ReqRespMethod.DataColumnSidecarsByRange]: fulu.DataColumnSidecarsByRangeRequest;
@@ -82,6 +84,7 @@ type ResponseBodyByMethod = {
82
84
  // Do not matter
83
85
  [ReqRespMethod.BeaconBlocksByRange]: SignedBeaconBlock;
84
86
  [ReqRespMethod.BeaconBlocksByRoot]: SignedBeaconBlock;
87
+ [ReqRespMethod.BeaconBlocksByHead]: SignedBeaconBlock;
85
88
  [ReqRespMethod.BlobSidecarsByRange]: deneb.BlobSidecar;
86
89
  [ReqRespMethod.BlobSidecarsByRoot]: deneb.BlobSidecar;
87
90
  [ReqRespMethod.DataColumnSidecarsByRange]: DataColumnSidecar;
@@ -111,6 +114,7 @@ export const requestSszTypeByMethod: (
111
114
 
112
115
  [ReqRespMethod.BeaconBlocksByRange]: ssz.phase0.BeaconBlocksByRangeRequest,
113
116
  [ReqRespMethod.BeaconBlocksByRoot]: BeaconBlocksByRootRequestType(fork, config),
117
+ [ReqRespMethod.BeaconBlocksByHead]: ssz.fulu.BeaconBlocksByHeadRequest,
114
118
  [ReqRespMethod.BlobSidecarsByRange]: ssz.deneb.BlobSidecarsByRangeRequest,
115
119
  [ReqRespMethod.BlobSidecarsByRoot]: BlobSidecarsByRootRequestType(fork, config),
116
120
  [ReqRespMethod.DataColumnSidecarsByRange]: ssz.fulu.DataColumnSidecarsByRangeRequest,
@@ -142,6 +146,7 @@ export const responseSszTypeByMethod: {[K in ReqRespMethod]: ResponseTypeGetter<
142
146
  version === Version.V1 ? ssz.phase0.Metadata : version === Version.V2 ? ssz.altair.Metadata : ssz.fulu.Metadata,
143
147
  [ReqRespMethod.BeaconBlocksByRange]: blocksResponseType,
144
148
  [ReqRespMethod.BeaconBlocksByRoot]: blocksResponseType,
149
+ [ReqRespMethod.BeaconBlocksByHead]: (fork) => ssz[fork].SignedBeaconBlock,
145
150
  [ReqRespMethod.BlobSidecarsByRange]: () => ssz.deneb.BlobSidecar,
146
151
  [ReqRespMethod.BlobSidecarsByRoot]: () => ssz.deneb.BlobSidecar,
147
152
  [ReqRespMethod.LightClientBootstrap]: (fork) => sszTypesFor(onlyPostAltairFork(fork)).LightClientBootstrap,
@@ -7,7 +7,11 @@ export const MIN_FINALIZED_CHAIN_VALIDATED_EPOCHS = 10;
7
7
  /** The number of times to retry a batch before it is considered failed. */
8
8
  export const MAX_BATCH_DOWNLOAD_ATTEMPTS = 5;
9
9
 
10
- /** Backoff before assigning more range-sync batches to a peer that rate-limited us. */
10
+ /**
11
+ * Backoff before assigning more range-sync batches to a peer that rate-limited us.
12
+ *
13
+ * Note: this is used when rate limited due to MAX_CONCURRENT_REQUESTS
14
+ */
11
15
  export const RATE_LIMITED_PEER_BACKOFF_MS = 5_000;
12
16
 
13
17
  /**
@@ -45,6 +45,15 @@ export type Attempt = {
45
45
  hash: RootHex;
46
46
  };
47
47
 
48
+ type TrackedRequest = {
49
+ /** only happen for the 1st batch in checkpoint sync */
50
+ parentPayload: boolean;
51
+ /**
52
+ * we always issue by_range before parent_payload, so we don't model this as null
53
+ */
54
+ byRangeColumns: Set<number>;
55
+ };
56
+
48
57
  export type AwaitingDownloadState = {
49
58
  status: BatchStatus.AwaitingDownload;
50
59
  blocks: IBlockInput[];
@@ -62,6 +71,7 @@ export type BatchState =
62
71
  | {
63
72
  status: BatchStatus.Downloading;
64
73
  peer: PeerIdStr;
74
+ request: TrackedRequest;
65
75
  blocks: IBlockInput[];
66
76
  payloadEnvelopes: Map<Slot, PayloadEnvelopeInput> | null;
67
77
  }
@@ -110,6 +120,13 @@ function formatColumnsReq(req: {startSlot: Slot; count: number; columns: number[
110
120
  return `startSlot=${req.startSlot},count=${req.count},cols=${prettyPrintIndices(req.columns)}`;
111
121
  }
112
122
 
123
+ function getTrackedRequest({parentPayloadRequest, columnsRequest}: DownloadByRangeRequests): TrackedRequest {
124
+ return {
125
+ parentPayload: parentPayloadRequest != null,
126
+ byRangeColumns: new Set(parentPayloadRequest == null ? (columnsRequest?.columns ?? []) : []),
127
+ };
128
+ }
129
+
113
130
  /**
114
131
  * Batches are downloaded at the first block of the epoch.
115
132
  *
@@ -131,8 +148,8 @@ export class Batch {
131
148
  requests: DownloadByRangeRequests;
132
149
  /** State of the batch. */
133
150
  state: BatchState = {status: BatchStatus.AwaitingDownload, blocks: [], payloadEnvelopes: null};
134
- /** Peers that provided good data */
135
- goodPeers: PeerIdStr[] = [];
151
+ /** Peers that provided good data, with column coverage for by_range requests */
152
+ private readonly successfulDownloads = new Map<PeerIdStr, TrackedRequest>();
136
153
  /** The `Attempts` that have been made and failed to send us this batch. */
137
154
  readonly failedProcessingAttempts: Attempt[] = [];
138
155
  /** The `Attempts` that have been made and failed because of execution malfunction. */
@@ -406,6 +423,35 @@ export class Batch {
406
423
  return [...this.failedDownloadAttempts, ...this.failedProcessingAttempts.flatMap((a) => a.peers)];
407
424
  }
408
425
 
426
+ /**
427
+ * True only if the peer has already returned a successful response for the current request.
428
+ * A by_range success may update `this.requests` to parent_payload, and the same peer is then
429
+ * still eligible for the newly discovered parent payload data.
430
+ * For by_range, a peer that previously succeeded with a superset of requested columns is skipped.
431
+ */
432
+ hasPeerSucceededCurrentRequest(peer: PeerSyncMeta): boolean {
433
+ const successfulDownload = this.successfulDownloads.get(peer.peerId);
434
+ if (successfulDownload == null) return false;
435
+
436
+ const request = getTrackedRequest(this.getRequestsForPeer(peer));
437
+ if (request.parentPayload) return successfulDownload.parentPayload;
438
+
439
+ const requestByRangeColumns = request.byRangeColumns;
440
+
441
+ if (requestByRangeColumns.size === 0) {
442
+ // this means a download blocks/envelops by_range only
443
+ // don't do that again if we already did it
444
+ // see https://github.com/ChainSafe/lodestar/issues/9357
445
+ return true;
446
+ }
447
+
448
+ return [...requestByRangeColumns].every((column) => successfulDownload.byRangeColumns.has(column));
449
+ }
450
+
451
+ private getSuccessfulPeers(): PeerIdStr[] {
452
+ return Array.from(this.successfulDownloads.keys());
453
+ }
454
+
409
455
  getMetadata(): BatchMetadata {
410
456
  const {blocksRequest, blobsRequest, columnsRequest, envelopesRequest} = this.requests;
411
457
  const failedProcessingPeerList = this.failedProcessingAttempts.flatMap((a) => a.peers);
@@ -440,14 +486,17 @@ export class Batch {
440
486
  /**
441
487
  * AwaitingDownload -> Downloading
442
488
  */
443
- startDownloading(peer: PeerIdStr): void {
489
+ startDownloading(peer: PeerSyncMeta): void {
444
490
  if (this.state.status !== BatchStatus.AwaitingDownload) {
445
491
  throw new BatchError(this.wrongStatusErrorType(BatchStatus.AwaitingDownload));
446
492
  }
447
493
 
494
+ const request = getTrackedRequest(this.getRequestsForPeer(peer));
495
+
448
496
  this.state = {
449
497
  status: BatchStatus.Downloading,
450
- peer,
498
+ peer: peer.peerId,
499
+ request,
451
500
  blocks: this.state.blocks,
452
501
  payloadEnvelopes: this.state.payloadEnvelopes,
453
502
  };
@@ -468,7 +517,17 @@ export class Batch {
468
517
  // ensure that blocks are always sorted before getting stored on the batch.state or being used to getRequests
469
518
  blocks.sort((a, b) => a.slot - b.slot);
470
519
 
471
- this.goodPeers.push(peer);
520
+ const successfulDownload = this.successfulDownloads.get(peer) ?? {
521
+ parentPayload: false,
522
+ byRangeColumns: new Set<number>(),
523
+ };
524
+ successfulDownload.parentPayload ||= this.state.request.parentPayload;
525
+ if (!this.state.request.parentPayload) {
526
+ for (const column of this.state.request.byRangeColumns) {
527
+ successfulDownload.byRangeColumns.add(column);
528
+ }
529
+ }
530
+ this.successfulDownloads.set(peer, successfulDownload);
472
531
 
473
532
  let allComplete = true;
474
533
  const slots = new Set<number>();
@@ -497,8 +556,9 @@ export class Batch {
497
556
  if (allComplete && isForkPostGloas(this.forkName)) {
498
557
  for (const block of blocks) {
499
558
  const payloadInput = newPayloadEnvelopes?.get(block.slot);
500
- // by_range needs every block's envelope and all sampled columns.
501
- if (!payloadInput?.hasPayloadEnvelope() || !payloadInput.hasComputedAllData()) {
559
+ // only need to make sure envelope has all columns, not all blocks have payload
560
+ // assertLinearChainSegment() was called before reaching this
561
+ if (payloadInput?.hasPayloadEnvelope() && !payloadInput.hasComputedAllData()) {
502
562
  allComplete = false;
503
563
  break;
504
564
  }
@@ -589,10 +649,10 @@ export class Batch {
589
649
  const blocks = this.state.blocks;
590
650
  const payloadEnvelopes = this.state.payloadEnvelopes;
591
651
  const hash = hashBlocks(blocks, this.config); // tracks blocks to report peer on processing error
592
- // Reset goodPeers in case another download attempt needs to be made. When Attempt is successful or not the peers
593
- // that the data came from will be handled by the Attempt that goes for processing
594
- const peers = this.goodPeers;
595
- this.goodPeers = [];
652
+ // Reset successfulDownloads in case another download attempt needs to be made. When Attempt is successful or not
653
+ // the peers that the data came from will be handled by the Attempt that goes for processing.
654
+ const peers = this.getSuccessfulPeers();
655
+ this.successfulDownloads.clear();
596
656
  this.state = {status: BatchStatus.Processing, blocks, payloadEnvelopes, attempt: {peers, hash}};
597
657
  return {blocks, payloadEnvelopes, peers};
598
658
  }
@@ -1,5 +1,4 @@
1
1
  import {ChainForkConfig} from "@lodestar/config";
2
- import {RequestErrorCode} from "@lodestar/reqresp";
3
2
  import {Epoch, Root, Slot, gloas} from "@lodestar/types";
4
3
  import {ErrorAborted, LodestarError, Logger, prettyPrintIndices, toRootHex} from "@lodestar/utils";
5
4
  import {isBlockInputBlobs, isBlockInputColumns} from "../../chain/blocks/blockInput/blockInput.js";
@@ -16,13 +15,9 @@ import {CustodyConfig} from "../../util/dataColumns.js";
16
15
  import {ItTrigger} from "../../util/itTrigger.js";
17
16
  import {PeerIdStr} from "../../util/peerId.js";
18
17
  import {WarnResult, wrapError} from "../../util/wrapError.js";
19
- import {
20
- BATCH_BUFFER_SIZE,
21
- EPOCHS_PER_BATCH,
22
- MAX_LOOK_AHEAD_EPOCHS,
23
- RATE_LIMITED_PEER_BACKOFF_MS,
24
- } from "../constants.js";
18
+ import {BATCH_BUFFER_SIZE, EPOCHS_PER_BATCH, MAX_LOOK_AHEAD_EPOCHS} from "../constants.js";
25
19
  import {DownloadByRangeError, DownloadByRangeErrorCode} from "../utils/downloadByRange.js";
20
+ import {getRateLimitedUntilMs} from "../utils/rateLimit.js";
26
21
  import {RangeSyncType} from "../utils/remoteSyncType.js";
27
22
  import {Batch, BatchError, BatchErrorCode, BatchMetadata, BatchStatus} from "./batch.js";
28
23
  import {
@@ -156,6 +151,7 @@ export class SyncChain {
156
151
  * The reqresp SelfRateLimiter independently enforces backoff at the protocol level as a safety net.
157
152
  */
158
153
  private readonly rateLimitedPeers = new Map<PeerIdStr, number>();
154
+ private rateLimitBackoffTimeout: NodeJS.Timeout | undefined;
159
155
 
160
156
  private readonly logger: Logger;
161
157
  private readonly config: ChainForkConfig;
@@ -241,6 +237,7 @@ export class SyncChain {
241
237
  */
242
238
  stopSyncing(): void {
243
239
  this.status = SyncChainStatus.Stopped;
240
+ this.clearRateLimitBackoffTimer();
244
241
  this.logger.debug("SyncChain stopSyncing", {id: this.logId});
245
242
  }
246
243
 
@@ -249,6 +246,7 @@ export class SyncChain {
249
246
  */
250
247
  remove(): void {
251
248
  this.logger.debug("SyncChain remove", {id: this.logId});
249
+ this.clearRateLimitBackoffTimer();
252
250
  this.batchProcessor.end(new ErrorAborted("SyncChain"));
253
251
  }
254
252
 
@@ -371,6 +369,8 @@ export class SyncChain {
371
369
  }
372
370
 
373
371
  throw e;
372
+ } finally {
373
+ this.clearRateLimitBackoffTimer();
374
374
  }
375
375
  }
376
376
 
@@ -394,6 +394,44 @@ export class SyncChain {
394
394
  }
395
395
  }
396
396
 
397
+ private scheduleRateLimitBackoffRetry(): void {
398
+ this.clearRateLimitBackoffTimer();
399
+
400
+ if (this.status !== SyncChainStatus.Syncing || this.rateLimitedPeers.size === 0) {
401
+ return;
402
+ }
403
+
404
+ const now = Date.now();
405
+ let retryAt: number | null = null;
406
+ for (const [peerId, rateLimitedUntil] of this.rateLimitedPeers.entries()) {
407
+ if (rateLimitedUntil <= now) {
408
+ this.rateLimitedPeers.delete(peerId);
409
+ continue;
410
+ }
411
+ retryAt = Math.min(retryAt ?? rateLimitedUntil, rateLimitedUntil);
412
+ }
413
+
414
+ if (retryAt === null) {
415
+ return;
416
+ }
417
+
418
+ this.rateLimitBackoffTimeout = setTimeout(
419
+ () => {
420
+ this.rateLimitBackoffTimeout = undefined;
421
+ this.triggerBatchDownloader();
422
+ this.scheduleRateLimitBackoffRetry();
423
+ },
424
+ Math.max(0, retryAt - now)
425
+ );
426
+ }
427
+
428
+ private clearRateLimitBackoffTimer(): void {
429
+ if (this.rateLimitBackoffTimeout !== undefined) {
430
+ clearTimeout(this.rateLimitBackoffTimeout);
431
+ this.rateLimitBackoffTimeout = undefined;
432
+ }
433
+ }
434
+
397
435
  /**
398
436
  * Attempts to request the next required batches from the peer pool if the chain is syncing.
399
437
  * It will exhaust the peer pool and left over batches until the batch buffer is reached.
@@ -513,7 +551,7 @@ export class SyncChain {
513
551
  peer: prettyPrintPeerIdStr(peer.peerId),
514
552
  });
515
553
  try {
516
- batch.startDownloading(peer.peerId);
554
+ batch.startDownloading(peer);
517
555
 
518
556
  // wrapError ensures to never call both batch success() and batch error()
519
557
  const res = await wrapError(this.downloadByRange(peer, batch, this.syncType));
@@ -543,6 +581,8 @@ export class SyncChain {
543
581
  case DownloadByRangeErrorCode.OUT_OF_ORDER_BLOCKS:
544
582
  case DownloadByRangeErrorCode.OUT_OF_RANGE_BLOCKS:
545
583
  case DownloadByRangeErrorCode.PARENT_ROOT_MISMATCH:
584
+ case DownloadByRangeErrorCode.INVALID_ENVELOPE_BEACON_BLOCK_ROOT:
585
+ case DownloadByRangeErrorCode.INVALID_CHAIN_SEGMENT:
546
586
  case BlobSidecarErrorCode.INCLUSION_PROOF_INVALID:
547
587
  case BlobSidecarErrorCode.INVALID_KZG_PROOF_BATCH:
548
588
  case DataColumnSidecarErrorCode.INCORRECT_KZG_COMMITMENTS_COUNT:
@@ -556,9 +596,11 @@ export class SyncChain {
556
596
  {id: this.logId, ...batch.getMetadata(), peer: prettyPrintPeerIdStr(peer.peerId)},
557
597
  res.err
558
598
  );
559
- if (errCode === RequestErrorCode.RESP_RATE_LIMITED || errCode === RequestErrorCode.REQUEST_SELF_RATE_LIMITED) {
599
+ const rateLimitedUntilMs = getRateLimitedUntilMs(res.err);
600
+ if (rateLimitedUntilMs !== null) {
560
601
  // Peer rate-limited us — don't count as a failed download attempt and mark peer for backoff
561
- this.rateLimitedPeers.set(peer.peerId, Date.now() + RATE_LIMITED_PEER_BACKOFF_MS);
602
+ this.rateLimitedPeers.set(peer.peerId, rateLimitedUntilMs);
603
+ this.scheduleRateLimitBackoffRetry();
562
604
  batch.downloadingRateLimited();
563
605
  this.triggerBatchDownloader();
564
606
  } else {