@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.
- package/lib/chain/blocks/utils/chainSegment.d.ts +1 -3
- package/lib/chain/blocks/utils/chainSegment.d.ts.map +1 -1
- package/lib/chain/blocks/utils/chainSegment.js +29 -26
- package/lib/chain/blocks/utils/chainSegment.js.map +1 -1
- package/lib/metrics/metrics/lodestar.d.ts +4 -0
- package/lib/metrics/metrics/lodestar.d.ts.map +1 -1
- package/lib/metrics/metrics/lodestar.js +5 -0
- package/lib/metrics/metrics/lodestar.js.map +1 -1
- package/lib/network/gossip/topic.d.ts +749 -2
- package/lib/network/gossip/topic.d.ts.map +1 -1
- package/lib/network/interface.d.ts +1 -0
- package/lib/network/interface.d.ts.map +1 -1
- package/lib/network/network.d.ts +1 -0
- package/lib/network/network.d.ts.map +1 -1
- package/lib/network/network.js +3 -0
- package/lib/network/network.js.map +1 -1
- package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
- package/lib/network/processor/gossipHandlers.js +31 -1
- package/lib/network/processor/gossipHandlers.js.map +1 -1
- package/lib/network/reqresp/ReqRespBeaconNode.d.ts.map +1 -1
- package/lib/network/reqresp/ReqRespBeaconNode.js +1 -1
- package/lib/network/reqresp/ReqRespBeaconNode.js.map +1 -1
- package/lib/network/reqresp/handlers/beaconBlocksByHead.d.ts +9 -0
- package/lib/network/reqresp/handlers/beaconBlocksByHead.d.ts.map +1 -0
- package/lib/network/reqresp/handlers/beaconBlocksByHead.js +61 -0
- package/lib/network/reqresp/handlers/beaconBlocksByHead.js.map +1 -0
- package/lib/network/reqresp/handlers/index.d.ts.map +1 -1
- package/lib/network/reqresp/handlers/index.js +5 -0
- package/lib/network/reqresp/handlers/index.js.map +1 -1
- package/lib/network/reqresp/interface.d.ts +1 -1
- package/lib/network/reqresp/interface.js +1 -1
- package/lib/network/reqresp/protocols.d.ts +1 -0
- package/lib/network/reqresp/protocols.d.ts.map +1 -1
- package/lib/network/reqresp/protocols.js +5 -0
- package/lib/network/reqresp/protocols.js.map +1 -1
- package/lib/network/reqresp/rateLimit.d.ts.map +1 -1
- package/lib/network/reqresp/rateLimit.js +4 -0
- package/lib/network/reqresp/rateLimit.js.map +1 -1
- package/lib/network/reqresp/score.d.ts.map +1 -1
- package/lib/network/reqresp/score.js +1 -0
- package/lib/network/reqresp/score.js.map +1 -1
- package/lib/network/reqresp/types.d.ts +3 -0
- package/lib/network/reqresp/types.d.ts.map +1 -1
- package/lib/network/reqresp/types.js +3 -0
- package/lib/network/reqresp/types.js.map +1 -1
- package/lib/sync/constants.d.ts +5 -1
- package/lib/sync/constants.d.ts.map +1 -1
- package/lib/sync/constants.js +5 -1
- package/lib/sync/constants.js.map +1 -1
- package/lib/sync/range/batch.d.ts +20 -3
- package/lib/sync/range/batch.d.ts.map +1 -1
- package/lib/sync/range/batch.js +54 -10
- package/lib/sync/range/batch.js.map +1 -1
- package/lib/sync/range/chain.d.ts +3 -0
- package/lib/sync/range/chain.d.ts.map +1 -1
- package/lib/sync/range/chain.js +44 -5
- package/lib/sync/range/chain.js.map +1 -1
- package/lib/sync/range/range.d.ts.map +1 -1
- package/lib/sync/range/range.js +40 -2
- package/lib/sync/range/range.js.map +1 -1
- package/lib/sync/range/utils/peerBalancer.d.ts +2 -1
- package/lib/sync/range/utils/peerBalancer.d.ts.map +1 -1
- package/lib/sync/range/utils/peerBalancer.js +8 -4
- package/lib/sync/range/utils/peerBalancer.js.map +1 -1
- package/lib/sync/unknownBlock.d.ts.map +1 -1
- package/lib/sync/unknownBlock.js +1 -12
- package/lib/sync/unknownBlock.js.map +1 -1
- package/lib/sync/utils/downloadByRange.d.ts +7 -1
- package/lib/sync/utils/downloadByRange.d.ts.map +1 -1
- package/lib/sync/utils/downloadByRange.js +2 -0
- package/lib/sync/utils/downloadByRange.js.map +1 -1
- package/lib/sync/utils/rateLimit.d.ts +2 -0
- package/lib/sync/utils/rateLimit.d.ts.map +1 -0
- package/lib/sync/utils/rateLimit.js +15 -0
- package/lib/sync/utils/rateLimit.js.map +1 -0
- package/package.json +16 -16
- package/src/chain/blocks/utils/chainSegment.ts +30 -27
- package/src/metrics/metrics/lodestar.ts +6 -0
- package/src/network/interface.ts +1 -0
- package/src/network/network.ts +12 -0
- package/src/network/processor/gossipHandlers.ts +35 -1
- package/src/network/reqresp/ReqRespBeaconNode.ts +1 -0
- package/src/network/reqresp/handlers/beaconBlocksByHead.ts +91 -0
- package/src/network/reqresp/handlers/index.ts +5 -0
- package/src/network/reqresp/interface.ts +1 -1
- package/src/network/reqresp/protocols.ts +6 -0
- package/src/network/reqresp/rateLimit.ts +4 -0
- package/src/network/reqresp/score.ts +1 -0
- package/src/network/reqresp/types.ts +5 -0
- package/src/sync/constants.ts +5 -1
- package/src/sync/range/batch.ts +71 -11
- package/src/sync/range/chain.ts +52 -10
- package/src/sync/range/range.ts +52 -2
- package/src/sync/range/utils/peerBalancer.ts +9 -3
- package/src/sync/unknownBlock.ts +1 -14
- package/src/sync/utils/downloadByRange.ts +8 -0
- 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: {
|
package/src/network/interface.ts
CHANGED
|
@@ -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(
|
package/src/network/network.ts
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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,
|
package/src/sync/constants.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
/**
|
package/src/sync/range/batch.ts
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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.
|
|
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
|
-
//
|
|
501
|
-
|
|
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
|
|
593
|
-
// that the data came from will be handled by the Attempt that goes for processing
|
|
594
|
-
const peers = this.
|
|
595
|
-
this.
|
|
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
|
}
|
package/src/sync/range/chain.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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,
|
|
602
|
+
this.rateLimitedPeers.set(peer.peerId, rateLimitedUntilMs);
|
|
603
|
+
this.scheduleRateLimitBackoffRetry();
|
|
562
604
|
batch.downloadingRateLimited();
|
|
563
605
|
this.triggerBatchDownloader();
|
|
564
606
|
} else {
|