@lodestar/beacon-node 1.43.0-dev.a45ba75824 → 1.43.0-dev.a58ad80359
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/api/impl/debug/index.d.ts.map +1 -1
- package/lib/api/impl/debug/index.js +0 -1
- package/lib/api/impl/debug/index.js.map +1 -1
- package/lib/api/impl/validator/index.d.ts.map +1 -1
- package/lib/api/impl/validator/index.js +2 -1
- package/lib/api/impl/validator/index.js.map +1 -1
- 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 +14 -18
- 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 +15 -6
- 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 +29 -17
- 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/types.d.ts +2 -1
- package/lib/chain/blocks/types.d.ts.map +1 -1
- package/lib/chain/blocks/utils/chainSegment.d.ts.map +1 -1
- package/lib/chain/blocks/utils/chainSegment.js +8 -0
- package/lib/chain/blocks/utils/chainSegment.js.map +1 -1
- package/lib/chain/blocks/verifyBlock.d.ts.map +1 -1
- package/lib/chain/blocks/verifyBlock.js +5 -6
- package/lib/chain/blocks/verifyBlock.js.map +1 -1
- package/lib/chain/blocks/verifyBlocksExecutionPayloads.d.ts +0 -4
- package/lib/chain/blocks/verifyBlocksExecutionPayloads.d.ts.map +1 -1
- package/lib/chain/blocks/verifyBlocksExecutionPayloads.js +5 -2
- package/lib/chain/blocks/verifyBlocksExecutionPayloads.js.map +1 -1
- package/lib/chain/blocks/verifyBlocksSanityChecks.d.ts +2 -1
- package/lib/chain/blocks/verifyBlocksSanityChecks.d.ts.map +1 -1
- package/lib/chain/blocks/verifyBlocksSanityChecks.js +16 -7
- package/lib/chain/blocks/verifyBlocksSanityChecks.js.map +1 -1
- package/lib/chain/blocks/verifyExecutionPayloadEnvelope.d.ts +2 -2
- package/lib/chain/blocks/verifyExecutionPayloadEnvelope.d.ts.map +1 -1
- package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js +10 -6
- 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/errors/proposerPreferences.d.ts +8 -1
- package/lib/chain/errors/proposerPreferences.d.ts.map +1 -1
- package/lib/chain/errors/proposerPreferences.js +1 -0
- package/lib/chain/errors/proposerPreferences.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/produceBlock/produceBlockBody.d.ts +1 -0
- package/lib/chain/produceBlock/produceBlockBody.d.ts.map +1 -1
- package/lib/chain/produceBlock/produceBlockBody.js +2 -7
- package/lib/chain/produceBlock/produceBlockBody.js.map +1 -1
- package/lib/chain/regen/queued.d.ts.map +1 -1
- package/lib/chain/regen/queued.js +1 -4
- package/lib/chain/regen/queued.js.map +1 -1
- package/lib/chain/regen/regen.d.ts.map +1 -1
- package/lib/chain/regen/regen.js +1 -4
- package/lib/chain/regen/regen.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 +8 -7
- package/lib/chain/seenCache/seenProposerPreferences.d.ts.map +1 -1
- package/lib/chain/seenCache/seenProposerPreferences.js +11 -10
- package/lib/chain/seenCache/seenProposerPreferences.js.map +1 -1
- package/lib/chain/validation/executionPayloadBid.js +11 -8
- package/lib/chain/validation/executionPayloadBid.js.map +1 -1
- package/lib/chain/validation/proposerPreferences.d.ts.map +1 -1
- package/lib/chain/validation/proposerPreferences.js +39 -17
- package/lib/chain/validation/proposerPreferences.js.map +1 -1
- package/lib/network/gossip/topic.d.ts +2 -0
- package/lib/network/gossip/topic.d.ts.map +1 -1
- package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
- package/lib/network/processor/gossipHandlers.js +14 -0
- package/lib/network/processor/gossipHandlers.js.map +1 -1
- package/lib/node/nodejs.js +2 -2
- package/lib/node/nodejs.js.map +1 -1
- package/lib/node/notifier.js +1 -7
- package/lib/node/notifier.js.map +1 -1
- package/lib/sync/constants.d.ts +3 -1
- package/lib/sync/constants.d.ts.map +1 -1
- package/lib/sync/constants.js +3 -4
- package/lib/sync/constants.js.map +1 -1
- package/lib/sync/range/batch.d.ts +12 -3
- package/lib/sync/range/batch.d.ts.map +1 -1
- package/lib/sync/range/batch.js +163 -33
- package/lib/sync/range/batch.js.map +1 -1
- package/lib/sync/range/chain.d.ts +13 -2
- package/lib/sync/range/chain.d.ts.map +1 -1
- package/lib/sync/range/chain.js +38 -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 +14 -3
- package/lib/sync/range/range.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 +138 -10
- package/lib/sync/unknownBlock.js.map +1 -1
- package/lib/sync/utils/downloadByRange.d.ts +29 -8
- package/lib/sync/utils/downloadByRange.d.ts.map +1 -1
- package/lib/sync/utils/downloadByRange.js +87 -31
- package/lib/sync/utils/downloadByRange.js.map +1 -1
- package/lib/util/sszBytes.d.ts.map +1 -1
- package/lib/util/sszBytes.js +8 -6
- package/lib/util/sszBytes.js.map +1 -1
- package/package.json +16 -16
- package/src/api/impl/debug/index.ts +0 -1
- package/src/api/impl/validator/index.ts +2 -1
- package/src/chain/blocks/blockInput/blockInput.ts +4 -1
- package/src/chain/blocks/importBlock.ts +14 -21
- package/src/chain/blocks/importExecutionPayload.ts +21 -11
- package/src/chain/blocks/index.ts +18 -9
- package/src/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.ts +31 -2
- package/src/chain/blocks/payloadEnvelopeInput/types.ts +17 -0
- package/src/chain/blocks/types.ts +2 -1
- package/src/chain/blocks/utils/chainSegment.ts +8 -0
- package/src/chain/blocks/verifyBlock.ts +7 -5
- package/src/chain/blocks/verifyBlocksExecutionPayloads.ts +6 -4
- package/src/chain/blocks/verifyBlocksSanityChecks.ts +16 -6
- package/src/chain/blocks/verifyExecutionPayloadEnvelope.ts +14 -6
- package/src/chain/chain.ts +29 -9
- package/src/chain/errors/proposerPreferences.ts +9 -1
- package/src/chain/initState.ts +9 -1
- package/src/chain/prepareNextSlot.ts +1 -1
- package/src/chain/produceBlock/produceBlockBody.ts +3 -7
- package/src/chain/regen/queued.ts +2 -7
- package/src/chain/regen/regen.ts +2 -7
- package/src/chain/seenCache/seenPayloadEnvelopeInput.ts +66 -20
- package/src/chain/seenCache/seenProposerPreferences.ts +14 -11
- package/src/chain/validation/executionPayloadBid.ts +11 -8
- package/src/chain/validation/proposerPreferences.ts +37 -18
- package/src/network/processor/gossipHandlers.ts +18 -0
- package/src/node/nodejs.ts +2 -2
- package/src/node/notifier.ts +1 -8
- package/src/sync/constants.ts +4 -4
- package/src/sync/range/batch.ts +187 -38
- package/src/sync/range/chain.ts +52 -5
- package/src/sync/range/range.ts +16 -3
- package/src/sync/sync.ts +13 -1
- package/src/sync/unknownBlock.ts +170 -10
- package/src/sync/utils/downloadByRange.ts +149 -33
- package/src/util/sszBytes.ts +8 -6
|
@@ -3,12 +3,11 @@ import {
|
|
|
3
3
|
computeEpochAtSlot,
|
|
4
4
|
createSingleSignatureSetFromComponents,
|
|
5
5
|
getProposerPreferencesSigningRoot,
|
|
6
|
-
isStatePostGloas,
|
|
7
6
|
} from "@lodestar/state-transition";
|
|
8
|
-
import {gloas} from "@lodestar/types";
|
|
7
|
+
import {ValidatorIndex, gloas} from "@lodestar/types";
|
|
8
|
+
import {toRootHex} from "@lodestar/utils";
|
|
9
9
|
import {GossipAction, ProposerPreferencesError, ProposerPreferencesErrorCode} from "../errors/index.js";
|
|
10
10
|
import {IBeaconChain} from "../index.js";
|
|
11
|
-
import {RegenCaller} from "../regen/index.js";
|
|
12
11
|
|
|
13
12
|
/**
|
|
14
13
|
* Validates a gossiped `SignedProposerPreferences` per
|
|
@@ -19,7 +18,8 @@ export async function validateGossipProposerPreferences(
|
|
|
19
18
|
signedProposerPreferences: gloas.SignedProposerPreferences
|
|
20
19
|
): Promise<void> {
|
|
21
20
|
const preferences = signedProposerPreferences.message;
|
|
22
|
-
const {proposalSlot, validatorIndex} = preferences;
|
|
21
|
+
const {proposalSlot, validatorIndex, dependentRoot} = preferences;
|
|
22
|
+
const dependentRootHex = toRootHex(dependentRoot);
|
|
23
23
|
const proposalEpoch = computeEpochAtSlot(proposalSlot);
|
|
24
24
|
|
|
25
25
|
// [IGNORE] `preferences.proposal_slot` is in the current or next epoch.
|
|
@@ -42,32 +42,51 @@ export async function validateGossipProposerPreferences(
|
|
|
42
42
|
});
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
// [IGNORE] The block with root `dependent_root` has been seen by the node.
|
|
46
|
+
// Resolve the proposer lookahead for the message's branch via head state (fast path) or
|
|
47
|
+
// the previous-root checkpoint state (populated by `processSlotsToNearestCheckpoint` for
|
|
48
|
+
// any imported branch crossing into `proposalEpoch - 1`). The head-state path also handles
|
|
49
|
+
// narrow timing windows where the checkpoint state isn't yet populated.
|
|
50
|
+
const headState = chain.getHeadState();
|
|
51
|
+
let proposers: ValidatorIndex[] | null = null;
|
|
52
|
+
if (headState.epoch === proposalEpoch && headState.currentDecisionRoot === dependentRootHex) {
|
|
53
|
+
proposers = headState.currentProposers;
|
|
54
|
+
} else if (headState.epoch === proposalEpoch - 1 && headState.nextDecisionRoot === dependentRootHex) {
|
|
55
|
+
proposers = headState.nextProposers;
|
|
56
|
+
} else {
|
|
57
|
+
// Sync lookup only to not trigger disk reload from gossip input.
|
|
58
|
+
const checkpointState = chain.regen.getCheckpointStateSync({epoch: proposalEpoch - 1, rootHex: dependentRootHex});
|
|
59
|
+
if (checkpointState !== null) {
|
|
60
|
+
// State is at `proposalEpoch - 1`, so proposers for `proposalSlot` (next epoch from
|
|
61
|
+
// the state's perspective) live in `nextProposers`.
|
|
62
|
+
proposers = checkpointState.nextProposers;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (proposers === null) {
|
|
66
|
+
throw new ProposerPreferencesError(GossipAction.IGNORE, {
|
|
67
|
+
code: ProposerPreferencesErrorCode.UNKNOWN_DEPENDENT_ROOT,
|
|
68
|
+
proposalSlot,
|
|
69
|
+
dependentRoot: dependentRootHex,
|
|
70
|
+
});
|
|
48
71
|
}
|
|
49
72
|
|
|
50
|
-
// [REJECT] `preferences
|
|
51
|
-
|
|
52
|
-
// returns True.
|
|
53
|
-
const epochOffset = proposalEpoch - state.epoch;
|
|
54
|
-
const proposers = epochOffset === 0 ? state.currentProposers : state.nextProposers;
|
|
55
|
-
const expectedProposer = proposers[proposalSlot % SLOTS_PER_EPOCH];
|
|
56
|
-
if (epochOffset < 0 || epochOffset > 1 || expectedProposer !== validatorIndex) {
|
|
73
|
+
// [REJECT] `is_valid_proposal_slot(state, preferences)` returns True.
|
|
74
|
+
if (proposers[proposalSlot % SLOTS_PER_EPOCH] !== validatorIndex) {
|
|
57
75
|
throw new ProposerPreferencesError(GossipAction.REJECT, {
|
|
58
76
|
code: ProposerPreferencesErrorCode.INVALID_PROPOSER,
|
|
59
77
|
proposalSlot,
|
|
60
78
|
validatorIndex,
|
|
79
|
+
dependentRoot: dependentRootHex,
|
|
61
80
|
});
|
|
62
81
|
}
|
|
63
82
|
|
|
64
|
-
// [IGNORE]
|
|
65
|
-
|
|
66
|
-
if (chain.seenProposerPreferences.isKnown(proposalSlot, validatorIndex)) {
|
|
83
|
+
// [IGNORE] First valid message for (dependent_root, proposal_slot, validator_index).
|
|
84
|
+
if (chain.seenProposerPreferences.isKnown(dependentRootHex, proposalSlot, validatorIndex)) {
|
|
67
85
|
throw new ProposerPreferencesError(GossipAction.IGNORE, {
|
|
68
86
|
code: ProposerPreferencesErrorCode.ALREADY_KNOWN,
|
|
69
87
|
proposalSlot,
|
|
70
88
|
validatorIndex,
|
|
89
|
+
dependentRoot: dependentRootHex,
|
|
71
90
|
});
|
|
72
91
|
}
|
|
73
92
|
|
|
@@ -87,5 +106,5 @@ export async function validateGossipProposerPreferences(
|
|
|
87
106
|
}
|
|
88
107
|
|
|
89
108
|
// Valid
|
|
90
|
-
chain.seenProposerPreferences.add(proposalSlot, validatorIndex);
|
|
109
|
+
chain.seenProposerPreferences.add(dependentRootHex, proposalSlot, validatorIndex);
|
|
91
110
|
}
|
|
@@ -607,6 +607,24 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
|
|
|
607
607
|
// Returns the delay between the start of `block.slot` and `current time`
|
|
608
608
|
const delaySec = chain.clock.secFromSlot(slot);
|
|
609
609
|
metrics?.gossipBlock.elapsedTimeTillProcessed.observe(delaySec);
|
|
610
|
+
|
|
611
|
+
if (isForkPostGloas(blockInput.forkName)) {
|
|
612
|
+
const payloadInput = chain.seenPayloadEnvelopeInputCache.get(blockInput.blockRootHex);
|
|
613
|
+
// This payloadInput should have been created just after gossip validation
|
|
614
|
+
if (!payloadInput) {
|
|
615
|
+
throw Error(
|
|
616
|
+
`PayloadEnvelopeInput not seeded for block ${blockInput.blockRootHex} during gossip processing`
|
|
617
|
+
);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Immediately attempt fetch of data columns from execution engine as the bid contains kzg commitments
|
|
621
|
+
// which is all the information we need so there is no reason to delay until execution payload arrives
|
|
622
|
+
// TODO GLOAS: If we want EL retries after this initial attempt, add an explicit retry policy here
|
|
623
|
+
// (for example later in the slot). Do not couple retries to incoming gossip columns.
|
|
624
|
+
// Columns fetched here feed payloadInput.addColumn, which resolves waitForAllData for any
|
|
625
|
+
// in-flight importExecutionPayload. No processExecutionPayload trigger needed from this path.
|
|
626
|
+
chain.getBlobsTracker.triggerGetBlobs(payloadInput);
|
|
627
|
+
}
|
|
610
628
|
})
|
|
611
629
|
.catch((e) => {
|
|
612
630
|
// Adjust verbosity based on error type
|
package/src/node/nodejs.ts
CHANGED
|
@@ -221,7 +221,7 @@ export class BeaconNode {
|
|
|
221
221
|
|
|
222
222
|
let executionEngineOpts = opts.executionEngine;
|
|
223
223
|
if (opts.executionEngine.mode === "mock") {
|
|
224
|
-
const
|
|
224
|
+
const latestEth1BlockHash =
|
|
225
225
|
isStatePostBellatrix(anchorState) && anchorState.isExecutionStateType
|
|
226
226
|
? isStatePostGloas(anchorState)
|
|
227
227
|
? toRootHex(anchorState.latestBlockHash)
|
|
@@ -230,7 +230,7 @@ export class BeaconNode {
|
|
|
230
230
|
executionEngineOpts = {
|
|
231
231
|
...opts.executionEngine,
|
|
232
232
|
genesisBlockHash: ZERO_HASH_HEX,
|
|
233
|
-
eth1BlockHash,
|
|
233
|
+
eth1BlockHash: opts.executionEngine.eth1BlockHash ?? latestEth1BlockHash,
|
|
234
234
|
genesisTime: anchorState.genesisTime,
|
|
235
235
|
config,
|
|
236
236
|
};
|
package/src/node/notifier.ts
CHANGED
|
@@ -167,14 +167,7 @@ function getHeadExecutionInfo(
|
|
|
167
167
|
return [];
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
-
|
|
171
|
-
// arrives, in that case the exec-block row surfaces the inherited parent anchor (from the
|
|
172
|
-
// bid), which is already validated. Normalize to "valid" to avoid leaking internal
|
|
173
|
-
// fork-choice bookkeeping into the log. Once the payload envelope arrives and the FULL
|
|
174
|
-
// variant becomes head, executionStatus is Valid/Syncing naturally.
|
|
175
|
-
// TODO GLOAS: revisit once optimistic sync is implemented
|
|
176
|
-
const executionStatusStr =
|
|
177
|
-
headInfo.executionStatus === ExecutionStatus.PayloadSeparated ? "valid" : headInfo.executionStatus.toLowerCase();
|
|
170
|
+
const executionStatusStr = headInfo.executionStatus.toLowerCase();
|
|
178
171
|
|
|
179
172
|
// Add execution status to notifier only if head is on/post bellatrix
|
|
180
173
|
if (isStatePostBellatrix(headState) && headState.isExecutionStateType) {
|
package/src/sync/constants.ts
CHANGED
|
@@ -5,10 +5,10 @@ export const PARALLEL_HEAD_CHAINS = 2;
|
|
|
5
5
|
export const MIN_FINALIZED_CHAIN_VALIDATED_EPOCHS = 10;
|
|
6
6
|
|
|
7
7
|
/** The number of times to retry a batch before it is considered failed. */
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
export const
|
|
8
|
+
export const MAX_BATCH_DOWNLOAD_ATTEMPTS = 5;
|
|
9
|
+
|
|
10
|
+
/** Backoff before assigning more range-sync batches to a peer that rate-limited us. */
|
|
11
|
+
export const RATE_LIMITED_PEER_BACKOFF_MS = 5_000;
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Consider batch faulty after downloading and processing this number of times
|
package/src/sync/range/batch.ts
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import {ChainForkConfig} from "@lodestar/config";
|
|
2
2
|
import {ForkName, isForkPostDeneb, isForkPostFulu, isForkPostGloas} from "@lodestar/params";
|
|
3
|
-
import {Epoch, RootHex, Slot, phase0} from "@lodestar/types";
|
|
4
|
-
import {LodestarError, prettyPrintIndices} from "@lodestar/utils";
|
|
3
|
+
import {Epoch, RootHex, SignedBeaconBlock, Slot, gloas, phase0} from "@lodestar/types";
|
|
4
|
+
import {LodestarError, byteArrayEquals, prettyPrintIndices, toRootHex} from "@lodestar/utils";
|
|
5
5
|
import {isBlockInputColumns} from "../../chain/blocks/blockInput/blockInput.js";
|
|
6
6
|
import {IBlockInput} from "../../chain/blocks/blockInput/types.js";
|
|
7
7
|
import {isDaOutOfRange} from "../../chain/blocks/blockInput/utils.js";
|
|
8
8
|
import {PayloadEnvelopeInput} from "../../chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.js";
|
|
9
9
|
import {BlockError, BlockErrorCode} from "../../chain/errors/index.js";
|
|
10
|
+
import {ZERO_HASH} from "../../constants/constants.js";
|
|
10
11
|
import {PeerSyncMeta} from "../../network/peers/peersData.js";
|
|
11
12
|
import {IClock} from "../../util/clock.js";
|
|
12
13
|
import {CustodyConfig} from "../../util/dataColumns.js";
|
|
13
14
|
import {PeerIdStr} from "../../util/peerId.js";
|
|
14
15
|
import {MAX_BATCH_DOWNLOAD_ATTEMPTS, MAX_BATCH_PROCESSING_ATTEMPTS} from "../constants.js";
|
|
15
|
-
import {DownloadByRangeRequests} from "../utils/downloadByRange.js";
|
|
16
|
+
import {DownloadByRangeRequests, ParentPayloadCommitments} from "../utils/downloadByRange.js";
|
|
16
17
|
import {getBatchSlotRange, hashBlocks} from "./utils/index.js";
|
|
17
18
|
|
|
18
19
|
/**
|
|
@@ -141,8 +142,18 @@ export class Batch {
|
|
|
141
142
|
private readonly config: ChainForkConfig;
|
|
142
143
|
private readonly clock: IClock;
|
|
143
144
|
private readonly custodyConfig: CustodyConfig;
|
|
144
|
-
|
|
145
|
-
|
|
145
|
+
private readonly isFirstBatchInChain: boolean;
|
|
146
|
+
private readonly latestBid: gloas.ExecutionPayloadBid | undefined;
|
|
147
|
+
|
|
148
|
+
constructor(
|
|
149
|
+
startEpoch: Epoch,
|
|
150
|
+
config: ChainForkConfig,
|
|
151
|
+
clock: IClock,
|
|
152
|
+
custodyConfig: CustodyConfig,
|
|
153
|
+
isFirstBatchInChain: boolean,
|
|
154
|
+
latestBid: gloas.ExecutionPayloadBid | undefined,
|
|
155
|
+
targetSlot: Slot
|
|
156
|
+
) {
|
|
146
157
|
this.config = config;
|
|
147
158
|
this.clock = clock;
|
|
148
159
|
this.custodyConfig = custodyConfig;
|
|
@@ -151,10 +162,40 @@ export class Batch {
|
|
|
151
162
|
this.forkName = this.config.getForkName(startSlot);
|
|
152
163
|
this.startEpoch = startEpoch;
|
|
153
164
|
this.startSlot = startSlot;
|
|
154
|
-
this.count = count;
|
|
165
|
+
this.count = Math.min(count, targetSlot - startSlot + 1);
|
|
166
|
+
this.isFirstBatchInChain = isFirstBatchInChain;
|
|
167
|
+
this.latestBid = latestBid;
|
|
155
168
|
this.requests = this.getRequests([]);
|
|
156
169
|
}
|
|
157
170
|
|
|
171
|
+
private shouldDownloadParentEnvelope(firstBlock?: SignedBeaconBlock): boolean {
|
|
172
|
+
if (!this.isFirstBatchInChain) return false;
|
|
173
|
+
|
|
174
|
+
if (this.startSlot === 0 || !isForkPostGloas(this.config.getForkName(this.startSlot - 1))) {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// we only know if we should download parent envelope if firstBlock is downloaded
|
|
179
|
+
if (firstBlock === undefined) return false;
|
|
180
|
+
if (this.latestBid === undefined) return false;
|
|
181
|
+
const firstBlockBidParentHash = (firstBlock.message.body as gloas.BeaconBlockBody).signedExecutionPayloadBid.message
|
|
182
|
+
.parentBlockHash;
|
|
183
|
+
return byteArrayEquals(firstBlockBidParentHash, this.latestBid.blockHash);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
getParentPayloadCommitments(parentBlockRoot: Uint8Array): ParentPayloadCommitments {
|
|
187
|
+
if (this.latestBid === undefined) {
|
|
188
|
+
throw new Error(
|
|
189
|
+
`Coding error: getParentPayloadCommitments called without latestBid for parentBlockRoot=${toRootHex(parentBlockRoot)}`
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
blockRoot: parentBlockRoot,
|
|
194
|
+
blockRootHex: toRootHex(parentBlockRoot),
|
|
195
|
+
kzgCommitments: this.latestBid.blobKzgCommitments,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
158
199
|
/**
|
|
159
200
|
* Builds ByRange requests for block, blobs and columns
|
|
160
201
|
*/
|
|
@@ -202,6 +243,7 @@ export class Batch {
|
|
|
202
243
|
const envelopesBySlot = this.state.payloadEnvelopes ?? new Map<Slot, PayloadEnvelopeInput>();
|
|
203
244
|
|
|
204
245
|
// ensure blocks are in slot-wise order
|
|
246
|
+
const isPostGloas = isForkPostGloas(this.forkName);
|
|
205
247
|
for (const blockInput of blocks) {
|
|
206
248
|
const blockSlot = blockInput.slot;
|
|
207
249
|
// check if block/data is present (hasBlock/hasAllData). If present then check if startSlot is the same as
|
|
@@ -217,21 +259,36 @@ export class Batch {
|
|
|
217
259
|
if (blockInput.hasBlock() && blockStartSlot === blockSlot) {
|
|
218
260
|
blockStartSlot = blockSlot + 1;
|
|
219
261
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
) {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
262
|
+
|
|
263
|
+
// Range sync uses hasComputedAllData (all sampled columns physically present), not hasAllData
|
|
264
|
+
// which flips at the reconstruction threshold. Sync never triggers reconstruction, so accepting
|
|
265
|
+
// a half-downloaded block here makes writeBlockInputToDb later block on waitForComputedAllData.
|
|
266
|
+
if (isPostGloas) {
|
|
267
|
+
// Post-Gloas: column data lives on PayloadEnvelopeInput, not on BlockInputNoData.
|
|
268
|
+
const payloadInput = envelopesBySlot.get(blockSlot);
|
|
269
|
+
if (blockInput.hasBlock() && envelopeStartSlot === blockSlot && payloadInput?.hasPayloadEnvelope()) {
|
|
270
|
+
envelopeStartSlot = blockSlot + 1;
|
|
271
|
+
}
|
|
272
|
+
if (payloadInput && !payloadInput.hasComputedAllData()) {
|
|
273
|
+
for (const index of payloadInput.getMissingSampledColumnMeta().missing) {
|
|
230
274
|
neededColumns.add(index);
|
|
231
275
|
}
|
|
276
|
+
} else if (payloadInput?.hasComputedAllData() && dataStartSlot === blockSlot) {
|
|
277
|
+
// Only advance dataStartSlot when we know columns for this slot are complete. If
|
|
278
|
+
// payloadInput is missing entirely we cannot tell, so stop here so the next round
|
|
279
|
+
// re-requests columns (and envelopes) starting at this slot.
|
|
280
|
+
dataStartSlot = blockSlot + 1;
|
|
281
|
+
}
|
|
282
|
+
} else {
|
|
283
|
+
if (isBlockInputColumns(blockInput) ? !blockInput.hasComputedAllData() : !blockInput.hasAllData()) {
|
|
284
|
+
if (isBlockInputColumns(blockInput)) {
|
|
285
|
+
for (const index of blockInput.getMissingSampledColumnMeta().missing) {
|
|
286
|
+
neededColumns.add(index);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
} else if (dataStartSlot === blockSlot) {
|
|
290
|
+
dataStartSlot = blockSlot + 1;
|
|
232
291
|
}
|
|
233
|
-
} else if (dataStartSlot === blockSlot) {
|
|
234
|
-
dataStartSlot = blockSlot + 1;
|
|
235
292
|
}
|
|
236
293
|
}
|
|
237
294
|
|
|
@@ -251,11 +308,15 @@ export class Batch {
|
|
|
251
308
|
// range of 40 - 63, startSlot will be inclusive but subtraction will exclusive so need to + 1
|
|
252
309
|
const count = endSlot - dataStartSlot + 1;
|
|
253
310
|
if (isForkPostFulu(this.forkName) && withinValidRequestWindow) {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
311
|
+
// Skip the column re-request when we have no specific column indices outstanding.
|
|
312
|
+
// Peer rejects an empty `columns` list
|
|
313
|
+
if (neededColumns.size > 0) {
|
|
314
|
+
requests.columnsRequest = {
|
|
315
|
+
count,
|
|
316
|
+
startSlot: dataStartSlot,
|
|
317
|
+
columns: Array.from(neededColumns),
|
|
318
|
+
};
|
|
319
|
+
}
|
|
259
320
|
} else if (isForkPostDeneb(this.forkName) && withinValidRequestWindow) {
|
|
260
321
|
requests.blobsRequest = {
|
|
261
322
|
count,
|
|
@@ -272,6 +333,36 @@ export class Batch {
|
|
|
272
333
|
};
|
|
273
334
|
}
|
|
274
335
|
|
|
336
|
+
// Only the first batch of a SyncChain may need the dangling-parent payload by-root.
|
|
337
|
+
if (blocks.length > 0 && this.shouldDownloadParentEnvelope(blocks[0].getBlock())) {
|
|
338
|
+
// shouldDownloadParentEnvelope() = true means there are at least 1 block
|
|
339
|
+
const parentRoot = blocks[0].getBlock().message.parentRoot;
|
|
340
|
+
if (!byteArrayEquals(parentRoot, ZERO_HASH)) {
|
|
341
|
+
const parentRootHex = toRootHex(parentRoot);
|
|
342
|
+
let parentPayloadInput: PayloadEnvelopeInput | undefined;
|
|
343
|
+
if (this.state.payloadEnvelopes) {
|
|
344
|
+
for (const pi of this.state.payloadEnvelopes.values()) {
|
|
345
|
+
if (pi.blockRootHex === parentRootHex) {
|
|
346
|
+
parentPayloadInput = pi;
|
|
347
|
+
break;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const needsEnvelope = !parentPayloadInput?.hasPayloadEnvelope();
|
|
353
|
+
const missingColumns = parentPayloadInput
|
|
354
|
+
? parentPayloadInput.getMissingSampledColumnMeta().missing
|
|
355
|
+
: this.custodyConfig.sampledColumns;
|
|
356
|
+
|
|
357
|
+
if (needsEnvelope || missingColumns.length > 0) {
|
|
358
|
+
requests.parentPayloadRequest = {
|
|
359
|
+
...(needsEnvelope ? {envelopeBlockRoot: parentRoot} : {}),
|
|
360
|
+
...(missingColumns.length > 0 ? {blockRoot: parentRoot, columns: missingColumns} : {}),
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
275
366
|
return requests;
|
|
276
367
|
}
|
|
277
368
|
|
|
@@ -283,24 +374,28 @@ export class Batch {
|
|
|
283
374
|
return this.requests;
|
|
284
375
|
}
|
|
285
376
|
|
|
286
|
-
// post-fulu we need to ensure that we only request columns that the peer has advertised
|
|
287
|
-
const {columnsRequest} = this.requests;
|
|
288
|
-
if (columnsRequest == null) {
|
|
289
|
-
return this.requests;
|
|
290
|
-
}
|
|
377
|
+
// post-fulu we need to ensure that we only request columns that the peer has advertised.
|
|
378
|
+
const {columnsRequest, parentPayloadRequest} = this.requests;
|
|
291
379
|
|
|
292
380
|
const peerColumns = new Set(peer.custodyColumns ?? []);
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
381
|
+
const filteredColumnsRequest =
|
|
382
|
+
columnsRequest != null ? columnsRequest.columns.filter((c) => peerColumns.has(c)) : null;
|
|
383
|
+
const parentColumns = parentPayloadRequest?.columns;
|
|
384
|
+
const filteredParentColumns = parentColumns != null ? parentColumns.filter((c) => peerColumns.has(c)) : null;
|
|
385
|
+
|
|
386
|
+
const updatedColumnRequest =
|
|
387
|
+
columnsRequest != null && filteredColumnsRequest != null
|
|
388
|
+
? {columnsRequest: {...columnsRequest, columns: filteredColumnsRequest}}
|
|
389
|
+
: {};
|
|
390
|
+
const updatedParentPayloadRequest =
|
|
391
|
+
parentPayloadRequest != null && filteredParentColumns != null
|
|
392
|
+
? {parentPayloadRequest: {...parentPayloadRequest, columns: filteredParentColumns}}
|
|
393
|
+
: {};
|
|
297
394
|
|
|
298
395
|
return {
|
|
299
396
|
...this.requests,
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
columns: requestedColumns,
|
|
303
|
-
},
|
|
397
|
+
...updatedColumnRequest,
|
|
398
|
+
...updatedParentPayloadRequest,
|
|
304
399
|
};
|
|
305
400
|
}
|
|
306
401
|
|
|
@@ -379,7 +474,11 @@ export class Batch {
|
|
|
379
474
|
const slots = new Set<number>();
|
|
380
475
|
for (const block of blocks) {
|
|
381
476
|
slots.add(block.slot);
|
|
382
|
-
|
|
477
|
+
const dataComplete = isBlockInputColumns(block)
|
|
478
|
+
? // by_range needs to download all columns
|
|
479
|
+
block.hasBlock() && block.hasComputedAllData()
|
|
480
|
+
: block.hasBlockAndAllData();
|
|
481
|
+
if (!dataComplete) {
|
|
383
482
|
allComplete = false;
|
|
384
483
|
}
|
|
385
484
|
}
|
|
@@ -395,11 +494,45 @@ export class Batch {
|
|
|
395
494
|
}
|
|
396
495
|
const newPayloadEnvelopes = payloadEnvelopes ?? this.state.payloadEnvelopes;
|
|
397
496
|
|
|
497
|
+
if (allComplete && isForkPostGloas(this.forkName)) {
|
|
498
|
+
for (const block of blocks) {
|
|
499
|
+
const payloadInput = newPayloadEnvelopes?.get(block.slot);
|
|
500
|
+
// by_range needs every block's envelope and all sampled columns.
|
|
501
|
+
if (!payloadInput?.hasPayloadEnvelope() || !payloadInput.hasComputedAllData()) {
|
|
502
|
+
allComplete = false;
|
|
503
|
+
break;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// First batch of a sync chain must additionally have the dangling-parent payload fully
|
|
509
|
+
// present, otherwise `processBlocks` will throw PARENT_PAYLOAD_UNKNOWN. The parent's
|
|
510
|
+
// `PayloadEnvelopeInput` is identified by `blockRootHex` matching `blocks[0].parentRoot`.
|
|
511
|
+
if (allComplete && blocks.length > 0 && this.shouldDownloadParentEnvelope(blocks[0].getBlock())) {
|
|
512
|
+
const parentRoot = blocks[0].getBlock().message.parentRoot;
|
|
513
|
+
// Genesis has no parent payload — nothing to wait for.
|
|
514
|
+
if (!byteArrayEquals(parentRoot, ZERO_HASH)) {
|
|
515
|
+
const parentRootHex = toRootHex(parentRoot);
|
|
516
|
+
let parentPayloadComplete = false;
|
|
517
|
+
if (newPayloadEnvelopes) {
|
|
518
|
+
for (const payloadInput of newPayloadEnvelopes.values()) {
|
|
519
|
+
if (payloadInput.blockRootHex === parentRootHex) {
|
|
520
|
+
parentPayloadComplete = payloadInput.hasPayloadEnvelope() && payloadInput.hasComputedAllData();
|
|
521
|
+
break;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
if (!parentPayloadComplete) {
|
|
526
|
+
allComplete = false;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
398
531
|
if (allComplete) {
|
|
399
532
|
this.state = {status: BatchStatus.AwaitingProcessing, blocks, payloadEnvelopes: newPayloadEnvelopes};
|
|
400
533
|
} else {
|
|
401
|
-
this.requests = this.getRequests(blocks);
|
|
402
534
|
this.state = {status: BatchStatus.AwaitingDownload, blocks, payloadEnvelopes: newPayloadEnvelopes};
|
|
535
|
+
this.requests = this.getRequests(blocks);
|
|
403
536
|
}
|
|
404
537
|
|
|
405
538
|
return this.state as DownloadSuccessState;
|
|
@@ -425,6 +558,22 @@ export class Batch {
|
|
|
425
558
|
};
|
|
426
559
|
}
|
|
427
560
|
|
|
561
|
+
/**
|
|
562
|
+
* Downloading -> AwaitingDownload (without counting as a failed attempt).
|
|
563
|
+
* Used when the peer rate-limited us — the request was never actually served.
|
|
564
|
+
*/
|
|
565
|
+
downloadingRateLimited(): void {
|
|
566
|
+
if (this.state.status !== BatchStatus.Downloading) {
|
|
567
|
+
throw new BatchError(this.wrongStatusErrorType(BatchStatus.Downloading));
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
this.state = {
|
|
571
|
+
status: BatchStatus.AwaitingDownload,
|
|
572
|
+
blocks: this.state.blocks,
|
|
573
|
+
payloadEnvelopes: this.state.payloadEnvelopes,
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
|
|
428
577
|
/**
|
|
429
578
|
* AwaitingProcessing -> Processing
|
|
430
579
|
*/
|
package/src/sync/range/chain.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {ChainForkConfig} from "@lodestar/config";
|
|
2
|
-
import {
|
|
2
|
+
import {RequestErrorCode} from "@lodestar/reqresp";
|
|
3
|
+
import {Epoch, Root, Slot, gloas} from "@lodestar/types";
|
|
3
4
|
import {ErrorAborted, LodestarError, Logger, prettyPrintIndices, toRootHex} from "@lodestar/utils";
|
|
4
5
|
import {isBlockInputBlobs, isBlockInputColumns} from "../../chain/blocks/blockInput/blockInput.js";
|
|
5
6
|
import {BlockInputErrorCode} from "../../chain/blocks/blockInput/errors.js";
|
|
@@ -15,7 +16,12 @@ import {CustodyConfig} from "../../util/dataColumns.js";
|
|
|
15
16
|
import {ItTrigger} from "../../util/itTrigger.js";
|
|
16
17
|
import {PeerIdStr} from "../../util/peerId.js";
|
|
17
18
|
import {WarnResult, wrapError} from "../../util/wrapError.js";
|
|
18
|
-
import {
|
|
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";
|
|
19
25
|
import {DownloadByRangeError, DownloadByRangeErrorCode} from "../utils/downloadByRange.js";
|
|
20
26
|
import {RangeSyncType} from "../utils/remoteSyncType.js";
|
|
21
27
|
import {Batch, BatchError, BatchErrorCode, BatchMetadata, BatchStatus} from "./batch.js";
|
|
@@ -139,20 +145,32 @@ export class SyncChain {
|
|
|
139
145
|
private readonly batchProcessor = new ItTrigger();
|
|
140
146
|
/** Sorted map of batches undergoing some kind of processing. */
|
|
141
147
|
private readonly batches = new Map<Epoch, Batch>();
|
|
148
|
+
/**
|
|
149
|
+
* `true` until the first `Batch` is constructed via `includeNextBatch`
|
|
150
|
+
*/
|
|
151
|
+
private isFirstBatch = true;
|
|
142
152
|
private readonly peerset = new Map<PeerIdStr, ChainTarget>();
|
|
153
|
+
/**
|
|
154
|
+
* Tracks peers that have rate-limited us, mapped to the timestamp (ms) until which we should avoid them.
|
|
155
|
+
* This is a sync-layer optimization to avoid assigning batches to backed-off peers.
|
|
156
|
+
* The reqresp SelfRateLimiter independently enforces backoff at the protocol level as a safety net.
|
|
157
|
+
*/
|
|
158
|
+
private readonly rateLimitedPeers = new Map<PeerIdStr, number>();
|
|
143
159
|
|
|
144
160
|
private readonly logger: Logger;
|
|
145
161
|
private readonly config: ChainForkConfig;
|
|
146
162
|
private readonly clock: IClock;
|
|
147
163
|
private readonly metrics: Metrics | null;
|
|
148
164
|
private readonly custodyConfig: CustodyConfig;
|
|
165
|
+
private readonly latestBid: gloas.ExecutionPayloadBid | undefined;
|
|
149
166
|
|
|
150
167
|
constructor(
|
|
151
168
|
initialBatchEpoch: Epoch,
|
|
152
169
|
initialTarget: ChainTarget,
|
|
153
170
|
syncType: RangeSyncType,
|
|
154
171
|
fns: SyncChainFns,
|
|
155
|
-
modules: SyncChainModules
|
|
172
|
+
modules: SyncChainModules,
|
|
173
|
+
latestBid: gloas.ExecutionPayloadBid | undefined
|
|
156
174
|
) {
|
|
157
175
|
const {config, clock, custodyConfig, logger, metrics} = modules;
|
|
158
176
|
this.firstBatchEpoch = initialBatchEpoch;
|
|
@@ -168,6 +186,7 @@ export class SyncChain {
|
|
|
168
186
|
this.clock = clock;
|
|
169
187
|
this.metrics = metrics;
|
|
170
188
|
this.custodyConfig = custodyConfig;
|
|
189
|
+
this.latestBid = latestBid;
|
|
171
190
|
this.logger = logger;
|
|
172
191
|
this.logId = `${syncType}-${nextChainId++}`;
|
|
173
192
|
|
|
@@ -248,6 +267,7 @@ export class SyncChain {
|
|
|
248
267
|
*/
|
|
249
268
|
removePeer(peerId: PeerIdStr): boolean {
|
|
250
269
|
const deleted = this.peerset.delete(peerId);
|
|
270
|
+
this.rateLimitedPeers.delete(peerId);
|
|
251
271
|
this.computeTarget();
|
|
252
272
|
return deleted;
|
|
253
273
|
}
|
|
@@ -383,8 +403,18 @@ export class SyncChain {
|
|
|
383
403
|
return;
|
|
384
404
|
}
|
|
385
405
|
|
|
406
|
+
const now = Date.now();
|
|
386
407
|
const peersSyncInfo: PeerSyncInfo[] = [];
|
|
387
408
|
for (const [peerId, target] of this.peerset.entries()) {
|
|
409
|
+
// Skip peers that are currently in rate-limit backoff
|
|
410
|
+
const rateLimitedUntil = this.rateLimitedPeers.get(peerId);
|
|
411
|
+
if (rateLimitedUntil !== undefined) {
|
|
412
|
+
if (now < rateLimitedUntil) {
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
this.rateLimitedPeers.delete(peerId);
|
|
416
|
+
}
|
|
417
|
+
|
|
388
418
|
try {
|
|
389
419
|
peersSyncInfo.push({...this.getConnectedPeerSyncMeta(peerId), target});
|
|
390
420
|
} catch (e) {
|
|
@@ -458,7 +488,17 @@ export class SyncChain {
|
|
|
458
488
|
return null;
|
|
459
489
|
}
|
|
460
490
|
|
|
461
|
-
const batch = new Batch(
|
|
491
|
+
const batch = new Batch(
|
|
492
|
+
startEpoch,
|
|
493
|
+
this.config,
|
|
494
|
+
this.clock,
|
|
495
|
+
this.custodyConfig,
|
|
496
|
+
this.isFirstBatch,
|
|
497
|
+
// `latestBid` is only meaningful for the first batch's parent-payload check
|
|
498
|
+
this.isFirstBatch ? this.latestBid : undefined,
|
|
499
|
+
this.target.slot
|
|
500
|
+
);
|
|
501
|
+
this.isFirstBatch = false;
|
|
462
502
|
this.batches.set(startEpoch, batch);
|
|
463
503
|
return batch;
|
|
464
504
|
}
|
|
@@ -516,7 +556,14 @@ export class SyncChain {
|
|
|
516
556
|
{id: this.logId, ...batch.getMetadata(), peer: prettyPrintPeerIdStr(peer.peerId)},
|
|
517
557
|
res.err
|
|
518
558
|
);
|
|
519
|
-
|
|
559
|
+
if (errCode === RequestErrorCode.RESP_RATE_LIMITED || errCode === RequestErrorCode.REQUEST_SELF_RATE_LIMITED) {
|
|
560
|
+
// 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);
|
|
562
|
+
batch.downloadingRateLimited();
|
|
563
|
+
this.triggerBatchDownloader();
|
|
564
|
+
} else {
|
|
565
|
+
batch.downloadingError(peer.peerId); // Throws after MAX_DOWNLOAD_ATTEMPTS
|
|
566
|
+
}
|
|
520
567
|
} else {
|
|
521
568
|
this.logger.verbose("Batch download success", {
|
|
522
569
|
id: this.logId,
|