@lodestar/beacon-node 1.43.0-dev.87cbe69c66 → 1.43.0-dev.8ad5d8be4c
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/blockInput/blockInput.d.ts +3 -0
- package/lib/chain/blocks/blockInput/blockInput.d.ts.map +1 -1
- package/lib/chain/blocks/blockInput/blockInput.js +4 -1
- package/lib/chain/blocks/blockInput/blockInput.js.map +1 -1
- package/lib/chain/blocks/importBlock.d.ts.map +1 -1
- package/lib/chain/blocks/importBlock.js +0 -16
- package/lib/chain/blocks/importBlock.js.map +1 -1
- package/lib/chain/blocks/importExecutionPayload.d.ts +4 -0
- package/lib/chain/blocks/importExecutionPayload.d.ts.map +1 -1
- package/lib/chain/blocks/importExecutionPayload.js +12 -1
- package/lib/chain/blocks/importExecutionPayload.js.map +1 -1
- package/lib/chain/blocks/index.d.ts.map +1 -1
- package/lib/chain/blocks/index.js +6 -7
- package/lib/chain/blocks/index.js.map +1 -1
- package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.d.ts +11 -1
- package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.d.ts.map +1 -1
- package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.js +23 -1
- package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.js.map +1 -1
- package/lib/chain/blocks/payloadEnvelopeInput/types.d.ts +16 -0
- package/lib/chain/blocks/payloadEnvelopeInput/types.d.ts.map +1 -1
- 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/chain/blocks/verifyExecutionPayloadEnvelope.d.ts.map +1 -1
- package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js +5 -4
- package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js.map +1 -1
- package/lib/chain/chain.d.ts.map +1 -1
- package/lib/chain/chain.js +25 -10
- package/lib/chain/chain.js.map +1 -1
- package/lib/chain/initState.d.ts.map +1 -1
- package/lib/chain/initState.js +6 -1
- package/lib/chain/initState.js.map +1 -1
- package/lib/chain/prepareNextSlot.js +1 -1
- package/lib/chain/prepareNextSlot.js.map +1 -1
- package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts +14 -10
- package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts.map +1 -1
- package/lib/chain/seenCache/seenPayloadEnvelopeInput.js +51 -17
- package/lib/chain/seenCache/seenPayloadEnvelopeInput.js.map +1 -1
- package/lib/chain/seenCache/seenProposerPreferences.d.ts +1 -1
- package/lib/chain/seenCache/seenProposerPreferences.d.ts.map +1 -1
- package/lib/chain/seenCache/seenProposerPreferences.js +6 -11
- package/lib/chain/seenCache/seenProposerPreferences.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 +45 -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 +7 -1
- package/lib/sync/constants.d.ts.map +1 -1
- package/lib/sync/constants.js +7 -4
- package/lib/sync/constants.js.map +1 -1
- package/lib/sync/range/batch.d.ts +32 -6
- package/lib/sync/range/batch.d.ts.map +1 -1
- package/lib/sync/range/batch.js +215 -41
- package/lib/sync/range/batch.js.map +1 -1
- package/lib/sync/range/chain.d.ts +16 -2
- package/lib/sync/range/chain.d.ts.map +1 -1
- package/lib/sync/range/chain.js +77 -4
- 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 +54 -5
- 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/sync.d.ts.map +1 -1
- package/lib/sync/sync.js +13 -0
- package/lib/sync/sync.js.map +1 -1
- package/lib/sync/unknownBlock.d.ts +7 -0
- package/lib/sync/unknownBlock.d.ts.map +1 -1
- package/lib/sync/unknownBlock.js +127 -10
- package/lib/sync/unknownBlock.js.map +1 -1
- package/lib/sync/utils/downloadByRange.d.ts +36 -9
- package/lib/sync/utils/downloadByRange.d.ts.map +1 -1
- package/lib/sync/utils/downloadByRange.js +89 -42
- 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/blockInput/blockInput.ts +4 -1
- package/src/chain/blocks/importBlock.ts +0 -18
- package/src/chain/blocks/importExecutionPayload.ts +18 -6
- package/src/chain/blocks/index.ts +5 -7
- package/src/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.ts +31 -2
- package/src/chain/blocks/payloadEnvelopeInput/types.ts +17 -0
- package/src/chain/blocks/utils/chainSegment.ts +30 -27
- package/src/chain/blocks/verifyExecutionPayloadEnvelope.ts +7 -4
- package/src/chain/chain.ts +29 -9
- package/src/chain/initState.ts +9 -1
- package/src/chain/prepareNextSlot.ts +1 -1
- package/src/chain/seenCache/seenPayloadEnvelopeInput.ts +66 -20
- package/src/chain/seenCache/seenProposerPreferences.ts +7 -12
- 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 +53 -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 +8 -4
- package/src/sync/range/batch.ts +256 -47
- package/src/sync/range/chain.ts +94 -5
- package/src/sync/range/range.ts +68 -5
- package/src/sync/range/utils/peerBalancer.ts +9 -3
- package/src/sync/sync.ts +13 -1
- package/src/sync/unknownBlock.ts +157 -10
- package/src/sync/utils/downloadByRange.ts +156 -44
- package/src/sync/utils/rateLimit.ts +16 -0
package/src/sync/range/range.ts
CHANGED
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
import {EventEmitter} from "node:events";
|
|
2
2
|
import {StrictEventEmitter} from "strict-event-emitter-types";
|
|
3
3
|
import {BeaconConfig} from "@lodestar/config";
|
|
4
|
-
import {computeStartSlotAtEpoch} from "@lodestar/state-transition";
|
|
4
|
+
import {IBeaconStateViewGloas, computeStartSlotAtEpoch, isStatePostGloas} from "@lodestar/state-transition";
|
|
5
5
|
import {Epoch, Status, fulu} from "@lodestar/types";
|
|
6
|
-
import {Logger, toRootHex} from "@lodestar/utils";
|
|
6
|
+
import {Logger, prettyPrintIndices, toRootHex} from "@lodestar/utils";
|
|
7
7
|
import {IBlockInput} from "../../chain/blocks/blockInput/types.js";
|
|
8
8
|
import {AttestationImportOpt, ImportBlockOpts} from "../../chain/blocks/index.js";
|
|
9
|
+
import {assertLinearChainSegment} from "../../chain/blocks/utils/chainSegment.js";
|
|
10
|
+
import {BlockError} from "../../chain/errors/index.js";
|
|
9
11
|
import {IBeaconChain} from "../../chain/index.js";
|
|
10
12
|
import {Metrics} from "../../metrics/index.js";
|
|
11
13
|
import {INetwork} from "../../network/index.js";
|
|
12
14
|
import {PeerIdStr} from "../../util/peerId.js";
|
|
13
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
DownloadByRangeError,
|
|
17
|
+
DownloadByRangeErrorCode,
|
|
18
|
+
cacheByRangeResponses,
|
|
19
|
+
downloadByRange,
|
|
20
|
+
} from "../utils/downloadByRange.js";
|
|
14
21
|
import {RangeSyncType, getRangeSyncTarget, rangeSyncTypes} from "../utils/remoteSyncType.js";
|
|
15
22
|
import {ChainTarget, SyncChain, SyncChainDebugState, SyncChainFns} from "./chain.js";
|
|
16
23
|
import {updateChains} from "./utils/index.js";
|
|
@@ -206,14 +213,18 @@ export class RangeSync extends (EventEmitter as {new (): RangeSyncEmitter}) {
|
|
|
206
213
|
|
|
207
214
|
private downloadByRange: SyncChainFns["downloadByRange"] = async (peer, batch) => {
|
|
208
215
|
const batchBlocks = batch.getBlocks();
|
|
216
|
+
const requests = batch.getRequestsForPeer(peer);
|
|
217
|
+
const parentRoot = requests.parentPayloadRequest?.envelopeBlockRoot ?? requests.parentPayloadRequest?.blockRoot;
|
|
218
|
+
const parentPayloadCommitments = parentRoot ? batch.getParentPayloadCommitments(parentRoot) : undefined;
|
|
209
219
|
const {result, warnings} = await downloadByRange({
|
|
210
220
|
config: this.config,
|
|
211
221
|
network: this.network,
|
|
212
222
|
logger: this.logger,
|
|
213
223
|
peerIdStr: peer.peerId,
|
|
214
224
|
batchBlocks,
|
|
225
|
+
parentPayloadCommitments,
|
|
215
226
|
peerDasMetrics: this.chain.metrics?.peerDas,
|
|
216
|
-
...
|
|
227
|
+
...requests,
|
|
217
228
|
});
|
|
218
229
|
const {responses, payloadEnvelopes: downloadedPayloadEnvelopes} = result;
|
|
219
230
|
const {blocks, payloadEnvelopes} = cacheByRangeResponses({
|
|
@@ -227,6 +238,49 @@ export class RangeSync extends (EventEmitter as {new (): RangeSyncEmitter}) {
|
|
|
227
238
|
custodyConfig: this.chain.custodyConfig,
|
|
228
239
|
seenTimestampSec: Date.now() / 1000,
|
|
229
240
|
});
|
|
241
|
+
|
|
242
|
+
const segmentBlocks = blocks.filter((b) => b.hasBlock()).sort((a, b) => a.slot - b.slot);
|
|
243
|
+
const envelopeSlots = payloadEnvelopes
|
|
244
|
+
? Array.from(payloadEnvelopes.entries())
|
|
245
|
+
.filter(([, pi]) => pi.hasPayloadEnvelope())
|
|
246
|
+
.map(([slot]) => slot)
|
|
247
|
+
.sort((a, b) => a - b)
|
|
248
|
+
: [];
|
|
249
|
+
this.logger.verbose("downloadByRange batch ready", {
|
|
250
|
+
peer: peer.peerId,
|
|
251
|
+
blockSlots: prettyPrintIndices(segmentBlocks.map((b) => b.slot)),
|
|
252
|
+
envelopeSlots: prettyPrintIndices(envelopeSlots),
|
|
253
|
+
...batch.getMetadata(),
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
if (segmentBlocks.length > 1) {
|
|
257
|
+
try {
|
|
258
|
+
assertLinearChainSegment(this.config, segmentBlocks, payloadEnvelopes, null);
|
|
259
|
+
} catch (err) {
|
|
260
|
+
if (err instanceof BlockError) {
|
|
261
|
+
this.logger.debug(
|
|
262
|
+
"downloadByRange segment validation failed",
|
|
263
|
+
{
|
|
264
|
+
peer: peer.peerId,
|
|
265
|
+
reason: err.type.code,
|
|
266
|
+
slot: err.signedBlock.message.slot,
|
|
267
|
+
detail: JSON.stringify(err.type),
|
|
268
|
+
...batch.getMetadata(),
|
|
269
|
+
},
|
|
270
|
+
err
|
|
271
|
+
);
|
|
272
|
+
// with this error, the peer will be penalized inside SyncChain
|
|
273
|
+
throw new DownloadByRangeError({
|
|
274
|
+
code: DownloadByRangeErrorCode.INVALID_CHAIN_SEGMENT,
|
|
275
|
+
slot: err.signedBlock.message.slot,
|
|
276
|
+
reason: err.type.code,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
throw err;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
230
284
|
return {result: {blocks, payloadEnvelopes}, warnings};
|
|
231
285
|
};
|
|
232
286
|
|
|
@@ -258,6 +312,14 @@ export class RangeSync extends (EventEmitter as {new (): RangeSyncEmitter}) {
|
|
|
258
312
|
private addPeerOrCreateChain(startEpoch: Epoch, target: ChainTarget, peer: PeerIdStr, syncType: RangeSyncType): void {
|
|
259
313
|
let syncChain = this.chains.get(syncType);
|
|
260
314
|
if (!syncChain) {
|
|
315
|
+
// The first batch of a new sync chain may need to detect whether the parent block was an
|
|
316
|
+
// gloas "empty" block (no envelope produced). It does so by comparing the first
|
|
317
|
+
// downloaded block's `bid.parentBlockHash` against the head state's `latestExecutionPayloadBid.blockHash`.
|
|
318
|
+
const headState = this.chain.getHeadState();
|
|
319
|
+
const latestBid = isStatePostGloas(headState)
|
|
320
|
+
? (headState as IBeaconStateViewGloas).latestExecutionPayloadBid
|
|
321
|
+
: undefined;
|
|
322
|
+
|
|
261
323
|
syncChain = new SyncChain(
|
|
262
324
|
startEpoch,
|
|
263
325
|
target,
|
|
@@ -276,7 +338,8 @@ export class RangeSync extends (EventEmitter as {new (): RangeSyncEmitter}) {
|
|
|
276
338
|
logger: this.logger,
|
|
277
339
|
custodyConfig: this.chain.custodyConfig,
|
|
278
340
|
metrics: this.metrics,
|
|
279
|
-
}
|
|
341
|
+
},
|
|
342
|
+
latestBid
|
|
280
343
|
);
|
|
281
344
|
this.chains.set(syncType, syncChain);
|
|
282
345
|
|
|
@@ -52,7 +52,8 @@ export class ChainPeersBalancer {
|
|
|
52
52
|
|
|
53
53
|
/**
|
|
54
54
|
* Return the most suitable peer to retry
|
|
55
|
-
* Sort peers by (1)
|
|
55
|
+
* Sort peers by (1) less active requests (2) most columns we need.
|
|
56
|
+
* Peers that failed this batch or already succeeded for the same request are excluded inside `filterPeers`.
|
|
56
57
|
*/
|
|
57
58
|
bestPeerToRetryBatch(batch: Batch): PeerSyncMeta | undefined {
|
|
58
59
|
if (batch.state.status !== BatchStatus.AwaitingDownload) {
|
|
@@ -63,10 +64,8 @@ export class ChainPeersBalancer {
|
|
|
63
64
|
const pendingDataColumns = columnsRequest?.columns ?? this.custodyConfig.sampledColumns;
|
|
64
65
|
const eligiblePeers = this.filterPeers(batch, pendingDataColumns, false);
|
|
65
66
|
|
|
66
|
-
const failedPeers = new Set(batch.getFailedPeers());
|
|
67
67
|
const sortedBestPeers = sortBy(
|
|
68
68
|
eligiblePeers,
|
|
69
|
-
({syncInfo}) => (failedPeers.has(syncInfo.peerId) ? 1 : 0), // prefer peers without failed requests
|
|
70
69
|
({syncInfo}) => this.activeRequestsByPeer.get(syncInfo.peerId) ?? 0, // prefer peers with least active req
|
|
71
70
|
({columns}) => -1 * columns // prefer peers with the most columns
|
|
72
71
|
);
|
|
@@ -117,9 +116,16 @@ export class ChainPeersBalancer {
|
|
|
117
116
|
return eligiblePeers;
|
|
118
117
|
}
|
|
119
118
|
|
|
119
|
+
// Skip peers that failed this batch, or that already returned the exact current request shape.
|
|
120
|
+
const failedPeers = new Set<PeerIdStr>(batch.getFailedPeers());
|
|
121
|
+
|
|
120
122
|
for (const peer of this.peers) {
|
|
121
123
|
const {earliestAvailableSlot, target, peerId} = peer;
|
|
122
124
|
|
|
125
|
+
if (failedPeers.has(peerId) || batch.hasPeerSucceededCurrentRequest(peer)) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
123
129
|
const activeRequest = this.activeRequestsByPeer.get(peerId) ?? 0;
|
|
124
130
|
if (noActiveRequest && activeRequest > 0) {
|
|
125
131
|
// consumer wants to find peer with no active request, but this peer has active request
|
package/src/sync/sync.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {SLOTS_PER_EPOCH} from "@lodestar/params";
|
|
2
2
|
import {Slot} from "@lodestar/types";
|
|
3
|
-
import {Logger} from "@lodestar/utils";
|
|
3
|
+
import {Logger, toRootHex} from "@lodestar/utils";
|
|
4
4
|
import {IBeaconChain} from "../chain/index.js";
|
|
5
5
|
import {GENESIS_SLOT} from "../constants/constants.js";
|
|
6
6
|
import {ExecutionEngineState} from "../execution/index.js";
|
|
@@ -188,6 +188,18 @@ export class BeaconSync implements IBeaconSync {
|
|
|
188
188
|
private addPeer = (data: NetworkEventData[NetworkEvent.peerConnected]): void => {
|
|
189
189
|
const localStatus = this.chain.getStatus();
|
|
190
190
|
const syncType = getPeerSyncType(localStatus, data.status, this.chain.forkChoice, this.slotImportTolerance);
|
|
191
|
+
this.logger.verbose("Peer sync type classified", {
|
|
192
|
+
peer: data.peer,
|
|
193
|
+
syncType,
|
|
194
|
+
localFinalizedEpoch: localStatus.finalizedEpoch,
|
|
195
|
+
localFinalizedRoot: toRootHex(localStatus.finalizedRoot),
|
|
196
|
+
localHeadSlot: localStatus.headSlot,
|
|
197
|
+
localHeadRoot: toRootHex(localStatus.headRoot),
|
|
198
|
+
remoteFinalizedEpoch: data.status.finalizedEpoch,
|
|
199
|
+
remoteFinalizedRoot: toRootHex(data.status.finalizedRoot),
|
|
200
|
+
remoteHeadSlot: data.status.headSlot,
|
|
201
|
+
remoteHeadRoot: toRootHex(data.status.headRoot),
|
|
202
|
+
});
|
|
191
203
|
|
|
192
204
|
// For metrics only
|
|
193
205
|
this.peerSyncType.set(data.peer, syncType);
|
package/src/sync/unknownBlock.ts
CHANGED
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
} from "./types.js";
|
|
43
43
|
import {DownloadByRootError, downloadByRoot} from "./utils/downloadByRoot.js";
|
|
44
44
|
import {getAllDescendantBlocks, getUnknownAndAncestorBlocks} from "./utils/pendingBlocksTree.js";
|
|
45
|
+
import {getRateLimitedUntilMs} from "./utils/rateLimit.js";
|
|
45
46
|
|
|
46
47
|
const MAX_ATTEMPTS_PER_BLOCK = 5;
|
|
47
48
|
const MAX_KNOWN_BAD_BLOCKS = 500;
|
|
@@ -63,6 +64,13 @@ enum FetchResult {
|
|
|
63
64
|
FailureMaxAttempts = "failure_max_attempts",
|
|
64
65
|
}
|
|
65
66
|
|
|
67
|
+
class UnknownBlockRateLimitedError extends Error {
|
|
68
|
+
constructor(message: string) {
|
|
69
|
+
super(message);
|
|
70
|
+
this.name = "UnknownBlockRateLimitedError";
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
66
74
|
/**
|
|
67
75
|
* BlockInputSync is a class that handles ReqResp to find blocks and data related to a specific blockRoot. The
|
|
68
76
|
* blockRoot may have been found via object gossip, or the API. Gossip objects that can trigger a search are block,
|
|
@@ -106,6 +114,7 @@ export class BlockInputSync {
|
|
|
106
114
|
private readonly maxPendingBlocks;
|
|
107
115
|
private subscribedToNetworkEvents = false;
|
|
108
116
|
private peerBalancer: UnknownBlockPeerBalancer;
|
|
117
|
+
private rateLimitBackoffTimeout: NodeJS.Timeout | undefined;
|
|
109
118
|
|
|
110
119
|
constructor(
|
|
111
120
|
private readonly config: ChainForkConfig,
|
|
@@ -155,6 +164,7 @@ export class BlockInputSync {
|
|
|
155
164
|
|
|
156
165
|
unsubscribeFromNetwork(): void {
|
|
157
166
|
this.logger.verbose("BlockInputSync disabled.");
|
|
167
|
+
this.clearRateLimitBackoffTimer();
|
|
158
168
|
this.chain.emitter.off(ChainEvent.unknownBlockRoot, this.onUnknownBlockRoot);
|
|
159
169
|
this.chain.emitter.off(ChainEvent.unknownEnvelopeBlockRoot, this.onUnknownEnvelopeBlockRoot);
|
|
160
170
|
this.chain.emitter.off(ChainEvent.incompleteBlockInput, this.onIncompleteBlockInput);
|
|
@@ -402,6 +412,7 @@ export class BlockInputSync {
|
|
|
402
412
|
private onPeerDisconnected = (data: NetworkEventData[NetworkEvent.peerDisconnected]): void => {
|
|
403
413
|
const peerId = data.peer;
|
|
404
414
|
this.peerBalancer.onPeerDisconnected(peerId);
|
|
415
|
+
this.scheduleRateLimitBackoffRetry();
|
|
405
416
|
};
|
|
406
417
|
|
|
407
418
|
/**
|
|
@@ -519,7 +530,7 @@ export class BlockInputSync {
|
|
|
519
530
|
*/
|
|
520
531
|
private triggerUnknownBlockSearch = (): void => {
|
|
521
532
|
// Cheap early stop to prevent calling the network.getConnectedPeers()
|
|
522
|
-
if (this.pendingBlocks.size === 0 && this.pendingPayloads.size === 0) {
|
|
533
|
+
if (!this.subscribedToNetworkEvents || (this.pendingBlocks.size === 0 && this.pendingPayloads.size === 0)) {
|
|
523
534
|
return;
|
|
524
535
|
}
|
|
525
536
|
|
|
@@ -607,6 +618,36 @@ export class BlockInputSync {
|
|
|
607
618
|
}
|
|
608
619
|
};
|
|
609
620
|
|
|
621
|
+
private scheduleRateLimitBackoffRetry(): void {
|
|
622
|
+
this.clearRateLimitBackoffTimer();
|
|
623
|
+
|
|
624
|
+
if (!this.subscribedToNetworkEvents || (this.pendingBlocks.size === 0 && this.pendingPayloads.size === 0)) {
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const now = Date.now();
|
|
629
|
+
const retryAt = this.peerBalancer.getNextRateLimitRetryAt();
|
|
630
|
+
if (retryAt === null) {
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
this.rateLimitBackoffTimeout = setTimeout(
|
|
635
|
+
() => {
|
|
636
|
+
this.rateLimitBackoffTimeout = undefined;
|
|
637
|
+
this.triggerUnknownBlockSearch();
|
|
638
|
+
this.scheduleRateLimitBackoffRetry();
|
|
639
|
+
},
|
|
640
|
+
Math.max(0, retryAt - now)
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
private clearRateLimitBackoffTimer(): void {
|
|
645
|
+
if (this.rateLimitBackoffTimeout !== undefined) {
|
|
646
|
+
clearTimeout(this.rateLimitBackoffTimeout);
|
|
647
|
+
this.rateLimitBackoffTimeout = undefined;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
610
651
|
private async downloadBlock(block: BlockInputSyncCacheItem): Promise<void> {
|
|
611
652
|
if (block.status !== PendingBlockInputStatus.pending) {
|
|
612
653
|
return;
|
|
@@ -678,6 +719,16 @@ export class BlockInputSync {
|
|
|
678
719
|
this.onUnknownBlockRoot({rootHex: pending.blockInput.parentRootHex, source: BlockInputSource.byRoot});
|
|
679
720
|
}
|
|
680
721
|
} else {
|
|
722
|
+
if (res.err instanceof UnknownBlockRateLimitedError) {
|
|
723
|
+
const pendingBlock = this.pendingBlocks.get(rootHex);
|
|
724
|
+
if (pendingBlock) {
|
|
725
|
+
pendingBlock.status = PendingBlockInputStatus.pending;
|
|
726
|
+
}
|
|
727
|
+
this.logger.debug("Deferring unknown block download due to peer rate limit", logCtx, res.err);
|
|
728
|
+
this.scheduleRateLimitBackoffRetry();
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
|
|
681
732
|
this.metrics?.blockInputSync.downloadedBlocksError.inc();
|
|
682
733
|
this.logger.debug("Ignoring unknown block root after many failed downloads", logCtx, res.err);
|
|
683
734
|
this.removeAndDownScoreAllDescendants(block);
|
|
@@ -994,12 +1045,19 @@ export class BlockInputSync {
|
|
|
994
1045
|
let envelope = payloadInput?.hasPayloadEnvelope() ? payloadInput.getPayloadEnvelope() : undefined;
|
|
995
1046
|
|
|
996
1047
|
let i = 0;
|
|
1048
|
+
let deferredByRateLimit = false;
|
|
997
1049
|
while (i++ < this.getMaxDownloadAttempts()) {
|
|
998
1050
|
const pendingColumns = payloadInput?.hasAllData()
|
|
999
1051
|
? new Set<number>()
|
|
1000
1052
|
: new Set(payloadInput?.getMissingSampledColumnMeta().missing ?? []);
|
|
1001
1053
|
const peerMeta = this.peerBalancer.bestPeerForPendingColumns(pendingColumns, excludedPeers);
|
|
1002
1054
|
if (peerMeta === null) {
|
|
1055
|
+
if (this.peerBalancer.getNextRateLimitRetryAt(pendingColumns, excludedPeers) !== null) {
|
|
1056
|
+
throw new UnknownBlockRateLimitedError(
|
|
1057
|
+
`Error fetching payload by root slot=${slot} root=${rootHex} after ${i}: peers with needed columns are rate-limited`
|
|
1058
|
+
);
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1003
1061
|
throw Error(
|
|
1004
1062
|
`Error fetching payload by root slot=${slot} root=${rootHex} after ${i}: cannot find peer with needed columns=${prettyPrintIndices(Array.from(pendingColumns))}`
|
|
1005
1063
|
);
|
|
@@ -1076,7 +1134,12 @@ export class BlockInputSync {
|
|
|
1076
1134
|
e as Error
|
|
1077
1135
|
);
|
|
1078
1136
|
|
|
1079
|
-
|
|
1137
|
+
const rateLimitedUntilMs = getRateLimitedUntilMs(e);
|
|
1138
|
+
if (rateLimitedUntilMs !== null) {
|
|
1139
|
+
deferredByRateLimit = true;
|
|
1140
|
+
this.peerBalancer.onRateLimited(peerId, rateLimitedUntilMs);
|
|
1141
|
+
this.scheduleRateLimitBackoffRetry();
|
|
1142
|
+
} else if (e instanceof RequestError) {
|
|
1080
1143
|
switch (e.type.code) {
|
|
1081
1144
|
case RequestErrorCode.REQUEST_RATE_LIMITED:
|
|
1082
1145
|
case RequestErrorCode.REQUEST_TIMEOUT:
|
|
@@ -1093,6 +1156,12 @@ export class BlockInputSync {
|
|
|
1093
1156
|
}
|
|
1094
1157
|
}
|
|
1095
1158
|
|
|
1159
|
+
if (deferredByRateLimit && this.peerBalancer.getNextRateLimitRetryAt() !== null) {
|
|
1160
|
+
throw new UnknownBlockRateLimitedError(
|
|
1161
|
+
`Error fetching payload with slot=${slot} root=${rootHex} after ${i - 1} attempts: peers are rate-limited`
|
|
1162
|
+
);
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1096
1165
|
throw Error(`Error fetching payload with slot=${slot} root=${rootHex} after ${i - 1} attempts.`);
|
|
1097
1166
|
}
|
|
1098
1167
|
|
|
@@ -1176,6 +1245,7 @@ export class BlockInputSync {
|
|
|
1176
1245
|
}
|
|
1177
1246
|
|
|
1178
1247
|
let i = 0;
|
|
1248
|
+
let deferredByRateLimit = false;
|
|
1179
1249
|
while (i++ < this.getMaxDownloadAttempts()) {
|
|
1180
1250
|
const pendingColumns =
|
|
1181
1251
|
isPendingBlockInput(cacheItem) && isBlockInputColumns(cacheItem.blockInput)
|
|
@@ -1183,6 +1253,12 @@ export class BlockInputSync {
|
|
|
1183
1253
|
: defaultPendingColumns;
|
|
1184
1254
|
const peerMeta = this.peerBalancer.bestPeerForPendingColumns(pendingColumns, excludedPeers);
|
|
1185
1255
|
if (peerMeta === null) {
|
|
1256
|
+
if (this.peerBalancer.getNextRateLimitRetryAt(pendingColumns, excludedPeers) !== null) {
|
|
1257
|
+
throw new UnknownBlockRateLimitedError(
|
|
1258
|
+
`Error fetching UnknownBlockRoot slot=${slot} root=${rootHex} after ${i}: peers with needed columns are rate-limited`
|
|
1259
|
+
);
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1186
1262
|
// no more peer with needed columns to try, throw error
|
|
1187
1263
|
const message = `Error fetching UnknownBlockRoot slot=${slot} root=${rootHex} after ${i}: cannot find peer with needed columns=${prettyPrintIndices(Array.from(pendingColumns))}`;
|
|
1188
1264
|
this.metrics?.blockInputSync.fetchTimeSec.observe(
|
|
@@ -1238,14 +1314,23 @@ export class BlockInputSync {
|
|
|
1238
1314
|
} else if (e instanceof RequestError) {
|
|
1239
1315
|
// should look into req_resp metrics in this case
|
|
1240
1316
|
downloadByRootMetrics?.error.inc({code: "req_resp", client: peerClient});
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1317
|
+
const rateLimitedUntilMs = getRateLimitedUntilMs(e);
|
|
1318
|
+
if (rateLimitedUntilMs !== null) {
|
|
1319
|
+
deferredByRateLimit = true;
|
|
1320
|
+
this.peerBalancer.onRateLimited(peerId, rateLimitedUntilMs);
|
|
1321
|
+
this.scheduleRateLimitBackoffRetry();
|
|
1322
|
+
} else {
|
|
1323
|
+
switch (e.type.code) {
|
|
1324
|
+
case RequestErrorCode.REQUEST_RATE_LIMITED:
|
|
1325
|
+
case RequestErrorCode.RESP_RATE_LIMITED:
|
|
1326
|
+
case RequestErrorCode.REQUEST_SELF_RATE_LIMITED:
|
|
1327
|
+
case RequestErrorCode.REQUEST_TIMEOUT:
|
|
1328
|
+
// do not exclude peer for these errors
|
|
1329
|
+
break;
|
|
1330
|
+
default:
|
|
1331
|
+
excludedPeers.add(peerId);
|
|
1332
|
+
break;
|
|
1333
|
+
}
|
|
1249
1334
|
}
|
|
1250
1335
|
} else {
|
|
1251
1336
|
// investigate if this happens
|
|
@@ -1273,6 +1358,10 @@ export class BlockInputSync {
|
|
|
1273
1358
|
|
|
1274
1359
|
const message = `Error fetching BlockInput with slot=${slot} root=${rootHex} after ${i - 1} attempts.`;
|
|
1275
1360
|
|
|
1361
|
+
if (deferredByRateLimit && this.peerBalancer.getNextRateLimitRetryAt() !== null) {
|
|
1362
|
+
throw new UnknownBlockRateLimitedError(`${message} Peers are rate-limited.`);
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1276
1365
|
if (!isPendingBlockInput(cacheItem)) {
|
|
1277
1366
|
throw Error(`${message} No block and no data was found.`);
|
|
1278
1367
|
}
|
|
@@ -1404,10 +1493,12 @@ export class BlockInputSync {
|
|
|
1404
1493
|
export class UnknownBlockPeerBalancer {
|
|
1405
1494
|
readonly peersMeta: Map<PeerIdStr, PeerSyncMeta>;
|
|
1406
1495
|
readonly activeRequests: Map<PeerIdStr, number>;
|
|
1496
|
+
readonly rateLimitedUntilByPeer: Map<PeerIdStr, number>;
|
|
1407
1497
|
|
|
1408
1498
|
constructor() {
|
|
1409
1499
|
this.peersMeta = new Map();
|
|
1410
1500
|
this.activeRequests = new Map();
|
|
1501
|
+
this.rateLimitedUntilByPeer = new Map();
|
|
1411
1502
|
}
|
|
1412
1503
|
|
|
1413
1504
|
/** Trigger on each peer re-status */
|
|
@@ -1422,6 +1513,41 @@ export class UnknownBlockPeerBalancer {
|
|
|
1422
1513
|
onPeerDisconnected(peerId: PeerIdStr): void {
|
|
1423
1514
|
this.peersMeta.delete(peerId);
|
|
1424
1515
|
this.activeRequests.delete(peerId);
|
|
1516
|
+
this.rateLimitedUntilByPeer.delete(peerId);
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
onRateLimited(peerId: PeerIdStr, rateLimitedUntilMs: number): void {
|
|
1520
|
+
this.rateLimitedUntilByPeer.set(peerId, rateLimitedUntilMs);
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
getNextRateLimitRetryAt(pendingColumns?: Set<number>, excludedPeers?: Set<PeerIdStr>): number | null {
|
|
1524
|
+
const now = Date.now();
|
|
1525
|
+
let retryAt: number | null = null;
|
|
1526
|
+
|
|
1527
|
+
for (const [peerId, rateLimitedUntil] of this.rateLimitedUntilByPeer.entries()) {
|
|
1528
|
+
if (rateLimitedUntil <= now) {
|
|
1529
|
+
this.rateLimitedUntilByPeer.delete(peerId);
|
|
1530
|
+
continue;
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
if (excludedPeers?.has(peerId)) {
|
|
1534
|
+
continue;
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
const syncMeta = this.peersMeta.get(peerId);
|
|
1538
|
+
if (syncMeta === undefined) {
|
|
1539
|
+
this.rateLimitedUntilByPeer.delete(peerId);
|
|
1540
|
+
continue;
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
if (pendingColumns !== undefined && !this.peerHasPendingColumns(syncMeta, pendingColumns)) {
|
|
1544
|
+
continue;
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
retryAt = Math.min(retryAt ?? rateLimitedUntil, rateLimitedUntil);
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
return retryAt;
|
|
1425
1551
|
}
|
|
1426
1552
|
|
|
1427
1553
|
/**
|
|
@@ -1470,6 +1596,7 @@ export class UnknownBlockPeerBalancer {
|
|
|
1470
1596
|
}
|
|
1471
1597
|
|
|
1472
1598
|
private filterPeers(pendingDataColumns: Set<number>, excludedPeers: Set<PeerIdStr>): PeerIdStr[] {
|
|
1599
|
+
const now = Date.now();
|
|
1473
1600
|
let maxColumnCount = 0;
|
|
1474
1601
|
const considerPeers: {peerId: PeerIdStr; columnCount: number}[] = [];
|
|
1475
1602
|
for (const [peerId, syncMeta] of this.peersMeta.entries()) {
|
|
@@ -1478,12 +1605,24 @@ export class UnknownBlockPeerBalancer {
|
|
|
1478
1605
|
continue;
|
|
1479
1606
|
}
|
|
1480
1607
|
|
|
1608
|
+
const rateLimitedUntil = this.rateLimitedUntilByPeer.get(peerId);
|
|
1609
|
+
if (rateLimitedUntil !== undefined) {
|
|
1610
|
+
if (now < rateLimitedUntil) {
|
|
1611
|
+
continue;
|
|
1612
|
+
}
|
|
1613
|
+
this.rateLimitedUntilByPeer.delete(peerId);
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1481
1616
|
const activeRequests = this.activeRequests.get(peerId) ?? 0;
|
|
1482
1617
|
if (activeRequests >= MAX_CONCURRENT_REQUESTS) {
|
|
1483
1618
|
// should return peer with no more than MAX_CONCURRENT_REQUESTS active requests
|
|
1484
1619
|
continue;
|
|
1485
1620
|
}
|
|
1486
1621
|
|
|
1622
|
+
if (!this.peerHasPendingColumns(syncMeta, pendingDataColumns)) {
|
|
1623
|
+
continue;
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1487
1626
|
if (pendingDataColumns.size === 0) {
|
|
1488
1627
|
considerPeers.push({peerId, columnCount: 0});
|
|
1489
1628
|
continue;
|
|
@@ -1517,4 +1656,12 @@ export class UnknownBlockPeerBalancer {
|
|
|
1517
1656
|
|
|
1518
1657
|
return eligiblePeers;
|
|
1519
1658
|
}
|
|
1659
|
+
|
|
1660
|
+
private peerHasPendingColumns(syncMeta: PeerSyncMeta, pendingDataColumns: Set<number>): boolean {
|
|
1661
|
+
if (pendingDataColumns.size === 0) {
|
|
1662
|
+
return true;
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
return syncMeta.custodyColumns.some((column) => pendingDataColumns.has(column));
|
|
1666
|
+
}
|
|
1520
1667
|
}
|