@lodestar/beacon-node 1.50.0-dev.d86161f63c → 1.50.0-dev.d99080f29a
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/beacon/blocks/index.d.ts.map +1 -1
- package/lib/api/impl/beacon/blocks/index.js +6 -1
- package/lib/api/impl/beacon/blocks/index.js.map +1 -1
- package/lib/chain/blocks/importBlock.d.ts.map +1 -1
- package/lib/chain/blocks/importBlock.js +4 -0
- package/lib/chain/blocks/importBlock.js.map +1 -1
- package/lib/chain/blocks/payloadEnvelopeInput/types.d.ts +2 -1
- package/lib/chain/blocks/payloadEnvelopeInput/types.d.ts.map +1 -1
- package/lib/chain/chain.d.ts +5 -4
- package/lib/chain/chain.d.ts.map +1 -1
- package/lib/chain/chain.js +11 -13
- package/lib/chain/chain.js.map +1 -1
- package/lib/chain/errors/envelopeReconstructionError.d.ts +5 -0
- package/lib/chain/errors/envelopeReconstructionError.d.ts.map +1 -1
- package/lib/chain/errors/envelopeReconstructionError.js +2 -0
- package/lib/chain/errors/envelopeReconstructionError.js.map +1 -1
- package/lib/chain/errors/gossipValidation.d.ts +3 -1
- package/lib/chain/errors/gossipValidation.d.ts.map +1 -1
- package/lib/chain/errors/gossipValidation.js +2 -0
- package/lib/chain/errors/gossipValidation.js.map +1 -1
- package/lib/chain/interface.d.ts +2 -2
- package/lib/chain/interface.d.ts.map +1 -1
- package/lib/chain/prepareNextSlot.d.ts.map +1 -1
- package/lib/chain/prepareNextSlot.js +5 -2
- package/lib/chain/prepareNextSlot.js.map +1 -1
- package/lib/chain/produceBlock/produceBlockBody.d.ts +3 -1
- package/lib/chain/produceBlock/produceBlockBody.d.ts.map +1 -1
- package/lib/chain/produceBlock/produceBlockBody.js +6 -2
- package/lib/chain/produceBlock/produceBlockBody.js.map +1 -1
- package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts +9 -2
- package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts.map +1 -1
- package/lib/chain/seenCache/seenPayloadEnvelopeInput.js +23 -13
- package/lib/chain/seenCache/seenPayloadEnvelopeInput.js.map +1 -1
- package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
- package/lib/network/processor/gossipHandlers.js +3 -0
- package/lib/network/processor/gossipHandlers.js.map +1 -1
- package/lib/network/processor/gossipValidatorFn.d.ts +7 -3
- package/lib/network/processor/gossipValidatorFn.d.ts.map +1 -1
- package/lib/network/processor/gossipValidatorFn.js +7 -3
- package/lib/network/processor/gossipValidatorFn.js.map +1 -1
- package/lib/network/processor/index.d.ts +9 -0
- package/lib/network/processor/index.d.ts.map +1 -1
- package/lib/network/processor/index.js +77 -10
- package/lib/network/processor/index.js.map +1 -1
- package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRoot.js +1 -1
- package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRoot.js.map +1 -1
- package/lib/sync/range/range.d.ts.map +1 -1
- package/lib/sync/range/range.js +50 -18
- package/lib/sync/range/range.js.map +1 -1
- package/lib/sync/unknownBlock.d.ts.map +1 -1
- package/lib/sync/unknownBlock.js +5 -4
- package/lib/sync/unknownBlock.js.map +1 -1
- package/lib/util/execution.d.ts +9 -11
- package/lib/util/execution.d.ts.map +1 -1
- package/lib/util/execution.js +5 -1
- package/lib/util/execution.js.map +1 -1
- package/package.json +15 -15
- package/src/api/impl/beacon/blocks/index.ts +7 -4
- package/src/chain/blocks/importBlock.ts +4 -0
- package/src/chain/blocks/payloadEnvelopeInput/types.ts +2 -1
- package/src/chain/chain.ts +14 -16
- package/src/chain/errors/envelopeReconstructionError.ts +3 -0
- package/src/chain/errors/gossipValidation.ts +2 -0
- package/src/chain/interface.ts +2 -2
- package/src/chain/prepareNextSlot.ts +6 -3
- package/src/chain/produceBlock/produceBlockBody.ts +8 -1
- package/src/chain/seenCache/seenPayloadEnvelopeInput.ts +31 -13
- package/src/network/processor/gossipHandlers.ts +3 -0
- package/src/network/processor/gossipValidatorFn.ts +13 -3
- package/src/network/processor/index.ts +89 -10
- package/src/network/reqresp/handlers/executionPayloadEnvelopesByRoot.ts +1 -1
- package/src/sync/range/range.ts +50 -18
- package/src/sync/unknownBlock.ts +5 -4
- package/src/util/execution.ts +16 -8
package/src/chain/chain.ts
CHANGED
|
@@ -71,7 +71,7 @@ import {BufferPool} from "../util/bufferPool.js";
|
|
|
71
71
|
import {Clock, ClockEvent, IClock} from "../util/clock.js";
|
|
72
72
|
import {CustodyConfig, getValidatorsCustodyRequirement} from "../util/dataColumns.js";
|
|
73
73
|
import {callInNextEventLoop} from "../util/eventLoop.js";
|
|
74
|
-
import {
|
|
74
|
+
import {ReconstructMissPolicy, isRebuildMiss, reconstructExecutionPayloadEnvelopes} from "../util/execution.js";
|
|
75
75
|
import {ensureDir, writeIfNotExist} from "../util/file.js";
|
|
76
76
|
import {isOptimisticBlock} from "../util/forkChoice.js";
|
|
77
77
|
import {JobItemQueue} from "../util/queue/itemQueue.js";
|
|
@@ -934,12 +934,13 @@ export class BeaconChain implements IBeaconChain {
|
|
|
934
934
|
|
|
935
935
|
/**
|
|
936
936
|
* Batch variant: archived header envelopes are rebuilt 32 per EL round-trip. Aligned with `requests`.
|
|
937
|
-
*
|
|
938
|
-
* returns null for that entry, for
|
|
937
|
+
* An archived envelope that cannot be rebuilt (EL-unavailable body or body root mismatch) is not the
|
|
938
|
+
* same as an unknown one: `"throw"` surfaces it, `"omit"` returns null for that entry, for
|
|
939
|
+
* peer-facing paths where the spec allows omission.
|
|
939
940
|
*/
|
|
940
941
|
async getSerializedExecutionPayloadEnvelopes(
|
|
941
942
|
requests: {blockSlot: Slot; blockRootHex: RootHex}[],
|
|
942
|
-
|
|
943
|
+
onMiss: ReconstructMissPolicy = "throw"
|
|
943
944
|
): Promise<(Uint8Array | null)[]> {
|
|
944
945
|
const out: (Uint8Array | null)[] = new Array(requests.length).fill(null);
|
|
945
946
|
const headerEnvelopes: gloas.SignedExecutionPayloadHeaderEnvelope[] = [];
|
|
@@ -978,14 +979,14 @@ export class BeaconChain implements IBeaconChain {
|
|
|
978
979
|
for (let j = 0; j < rebuilt.length; j++) {
|
|
979
980
|
const result = rebuilt[j];
|
|
980
981
|
if (isRebuildMiss(result)) {
|
|
981
|
-
if (
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
"Archived envelope failed body root check against EL bodies"
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
982
|
+
if (onMiss === "throw") throw result.error;
|
|
983
|
+
this.logger.debug(
|
|
984
|
+
result.reason === "mismatch"
|
|
985
|
+
? "Archived envelope failed body root check against EL bodies"
|
|
986
|
+
: "EL cannot serve bodies for archived envelope, omitting",
|
|
987
|
+
{slot: result.slot},
|
|
988
|
+
result.error
|
|
989
|
+
);
|
|
989
990
|
continue;
|
|
990
991
|
}
|
|
991
992
|
out[headerEnvelopeIdxs[j]] = ssz.gloas.SignedExecutionPayloadEnvelope.serialize(result);
|
|
@@ -1016,10 +1017,7 @@ export class BeaconChain implements IBeaconChain {
|
|
|
1016
1017
|
const [result] = await reconstructExecutionPayloadEnvelopes(this.executionEngine, this.metrics, [
|
|
1017
1018
|
archived.headerEnvelope,
|
|
1018
1019
|
]);
|
|
1019
|
-
if (isRebuildMiss(result))
|
|
1020
|
-
if (result.reason === "mismatch") throw result.error;
|
|
1021
|
-
return null;
|
|
1022
|
-
}
|
|
1020
|
+
if (isRebuildMiss(result)) throw result.error;
|
|
1023
1021
|
return result;
|
|
1024
1022
|
}
|
|
1025
1023
|
|
|
@@ -4,6 +4,8 @@ import {LodestarError} from "@lodestar/utils";
|
|
|
4
4
|
export enum EnvelopeReconstructionErrorCode {
|
|
5
5
|
/** The EL failed to answer engine_getPayloadBodiesByHashV2 (transport / EL down). Transient. */
|
|
6
6
|
ENGINE_UNAVAILABLE = "ENVELOPE_RECONSTRUCTION_ERROR_ENGINE_UNAVAILABLE",
|
|
7
|
+
/** The EL does not have the block or its block access list (pruned, or not yet re-indexed after a restart). May recover. */
|
|
8
|
+
BODY_UNAVAILABLE = "ENVELOPE_RECONSTRUCTION_ERROR_BODY_UNAVAILABLE",
|
|
7
9
|
/** A body returned by the EL does not hash to the root stored in the header envelope. Local DB/EL inconsistency. */
|
|
8
10
|
BODY_ROOT_MISMATCH = "ENVELOPE_RECONSTRUCTION_ERROR_BODY_ROOT_MISMATCH",
|
|
9
11
|
/** A by-range stream stopped short at a slot that cannot be served; the response so far is still valid */
|
|
@@ -12,6 +14,7 @@ export enum EnvelopeReconstructionErrorCode {
|
|
|
12
14
|
|
|
13
15
|
export type EnvelopeReconstructionErrorType =
|
|
14
16
|
| {code: EnvelopeReconstructionErrorCode.ENGINE_UNAVAILABLE}
|
|
17
|
+
| {code: EnvelopeReconstructionErrorCode.BODY_UNAVAILABLE; slot: Slot}
|
|
15
18
|
| {
|
|
16
19
|
code: EnvelopeReconstructionErrorCode.BODY_ROOT_MISMATCH;
|
|
17
20
|
slot: Slot;
|
|
@@ -9,6 +9,8 @@ export enum GossipErrorCode {
|
|
|
9
9
|
INVALID_SERIALIZED_BYTES_ERROR_CODE = "GOSSIP_ERROR_INVALID_SERIALIZED_BYTES",
|
|
10
10
|
PAST_SLOT = "GOSSIP_ERROR_PAST_SLOT",
|
|
11
11
|
FUTURE_SLOT = "GOSSIP_ERROR_FUTURE_SLOT",
|
|
12
|
+
FATAL_PEER = "GOSSIP_ERROR_FATAL_PEER",
|
|
13
|
+
INVALID_SLOT = "GOSSIP_ERROR_INVALID_SLOT",
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
export class GossipActionError<T extends {code: string}> extends LodestarError<T> {
|
package/src/chain/interface.ts
CHANGED
|
@@ -30,7 +30,7 @@ import {Metrics} from "../metrics/metrics.js";
|
|
|
30
30
|
import {BufferPool} from "../util/bufferPool.js";
|
|
31
31
|
import {IClock} from "../util/clock.js";
|
|
32
32
|
import {CustodyConfig} from "../util/dataColumns.js";
|
|
33
|
-
import {
|
|
33
|
+
import {ReconstructMissPolicy} from "../util/execution.js";
|
|
34
34
|
import {SerializedCache} from "../util/serializedCache.js";
|
|
35
35
|
import {IArchiveStore} from "./archiveStore/interface.js";
|
|
36
36
|
import {CheckpointBalancesCache} from "./balancesCache.js";
|
|
@@ -239,7 +239,7 @@ export interface IBeaconChain {
|
|
|
239
239
|
getSerializedExecutionPayloadEnvelope(blockSlot: Slot, blockRootHex: string): Promise<Uint8Array | null>;
|
|
240
240
|
getSerializedExecutionPayloadEnvelopes(
|
|
241
241
|
requests: {blockSlot: Slot; blockRootHex: RootHex}[],
|
|
242
|
-
|
|
242
|
+
onMiss?: ReconstructMissPolicy
|
|
243
243
|
): Promise<(Uint8Array | null)[]>;
|
|
244
244
|
getExecutionPayloadEnvelope(
|
|
245
245
|
blockSlot: Slot,
|
|
@@ -244,6 +244,10 @@ export class PrepareNextSlotScheduler {
|
|
|
244
244
|
parentBlockHash = preparedState.latestExecutionPayloadHeader.blockHash;
|
|
245
245
|
}
|
|
246
246
|
|
|
247
|
+
// The payload_attributes event must carry the same hashes we send to the EL
|
|
248
|
+
const safeBlockHash = getSafeExecutionBlockHash(this.chain.forkChoice, this.logger);
|
|
249
|
+
const finalizedBlockHash = getFinalizedExecutionBlockHash(this.chain.forkChoice);
|
|
250
|
+
|
|
247
251
|
let payloadAttributes: PayloadAttributes | undefined;
|
|
248
252
|
// If emitPayloadAttributes is true emit a SSE payloadAttributes event for
|
|
249
253
|
// every slot. Without the flag, only emit the event if we are proposing in the next slot.
|
|
@@ -256,6 +260,8 @@ export class PrepareNextSlotScheduler {
|
|
|
256
260
|
prepareSlot,
|
|
257
261
|
parentBlockRoot: fromHex(updatedHead.blockRoot),
|
|
258
262
|
parentBlockHash,
|
|
263
|
+
safeBlockHash,
|
|
264
|
+
finalizedBlockHash,
|
|
259
265
|
feeRecipient: feeRecipient ?? "0x0000000000000000000000000000000000000000",
|
|
260
266
|
});
|
|
261
267
|
this.chain.emitter.emit(routes.events.EventType.payloadAttributes, {data, version: fork});
|
|
@@ -267,9 +273,6 @@ export class PrepareNextSlotScheduler {
|
|
|
267
273
|
computeTimeAtSlot(this.config, prepareSlot, this.chain.genesisTime) - Date.now() / 1000;
|
|
268
274
|
this.metrics?.blockPayload.payloadAdvancePrepTime.observe(preparationTime);
|
|
269
275
|
|
|
270
|
-
const safeBlockHash = getSafeExecutionBlockHash(this.chain.forkChoice, this.logger);
|
|
271
|
-
const finalizedBlockHash = getFinalizedExecutionBlockHash(this.chain.forkChoice);
|
|
272
|
-
|
|
273
276
|
// awaiting here instead of throwing an async call because there is no other task
|
|
274
277
|
// left for scheduler and this gives nice semantics to catch and log errors in the
|
|
275
278
|
// try/catch wrapper here.
|
|
@@ -845,6 +845,8 @@ export function getPayloadAttributesForSSE(
|
|
|
845
845
|
prepareSlot,
|
|
846
846
|
parentBlockRoot,
|
|
847
847
|
parentBlockHash,
|
|
848
|
+
safeBlockHash,
|
|
849
|
+
finalizedBlockHash,
|
|
848
850
|
feeRecipient,
|
|
849
851
|
}: {
|
|
850
852
|
/**
|
|
@@ -855,6 +857,8 @@ export function getPayloadAttributesForSSE(
|
|
|
855
857
|
prepareSlot: Slot;
|
|
856
858
|
parentBlockRoot: Root;
|
|
857
859
|
parentBlockHash: Bytes32;
|
|
860
|
+
safeBlockHash: RootHex;
|
|
861
|
+
finalizedBlockHash: RootHex;
|
|
858
862
|
feeRecipient: string;
|
|
859
863
|
}
|
|
860
864
|
): SSEPayloadAttributes {
|
|
@@ -874,7 +878,10 @@ export function getPayloadAttributesForSSE(
|
|
|
874
878
|
payloadAttributes,
|
|
875
879
|
} as SSEPayloadAttributes;
|
|
876
880
|
|
|
877
|
-
if (
|
|
881
|
+
if (isForkPostGloas(fork)) {
|
|
882
|
+
(ssePayloadAttributes as gloas.SSEPayloadAttributes).safeBlockHash = fromHex(safeBlockHash);
|
|
883
|
+
(ssePayloadAttributes as gloas.SSEPayloadAttributes).finalizedBlockHash = fromHex(finalizedBlockHash);
|
|
884
|
+
} else {
|
|
878
885
|
// Removed in Gloas, builders can get the block number from the EL via the block hash if required
|
|
879
886
|
(ssePayloadAttributes as bellatrix.SSEPayloadAttributes).parentBlockNumber = prepareState.payloadBlockNumber;
|
|
880
887
|
}
|
|
@@ -47,8 +47,9 @@ export type SeenPayloadEnvelopeInputModules = {
|
|
|
47
47
|
* Created whenever we have a block because it needs block bid.
|
|
48
48
|
* Steady state (linear chain, healthy progression): the cache holds ~2 entries — the head
|
|
49
49
|
* (parent for next-slot production) and its parent (proposer-boost-reorg fallback). It can
|
|
50
|
-
*
|
|
51
|
-
*
|
|
50
|
+
* hold more during forks and range sync, or while an entry's payload is not FULL on the head's
|
|
51
|
+
* branch or its columns are still being gathered. Such entries are evicted on a later block
|
|
52
|
+
* import, on finalization or by the size cap.
|
|
52
53
|
*/
|
|
53
54
|
export class SeenPayloadEnvelopeInput {
|
|
54
55
|
private readonly config: ChainForkConfig;
|
|
@@ -264,18 +265,35 @@ export class SeenPayloadEnvelopeInput {
|
|
|
264
265
|
}
|
|
265
266
|
}
|
|
266
267
|
|
|
268
|
+
/**
|
|
269
|
+
* Removes a PayloadEnvelopeInput whose envelope failed import with a definitive invalid verdict. The envelope
|
|
270
|
+
* cannot be replaced in place, sync recreates the entry via `getOrReload()` or `add()` to fetch another one.
|
|
271
|
+
* No-op if the cached entry is a different object, it may already be the recreated one.
|
|
272
|
+
*/
|
|
273
|
+
removeInvalid(payloadInput: PayloadEnvelopeInput): void {
|
|
274
|
+
if (this.payloadInputs.get(payloadInput.blockRootHex) === payloadInput) {
|
|
275
|
+
this.evictPayloadInput(payloadInput, "invalid");
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
267
279
|
pruneBelowParent(parentBlock: ProtoBlock): void {
|
|
268
|
-
for (const
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
//
|
|
275
|
-
//
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
280
|
+
for (const input of this.payloadInputs.values()) {
|
|
281
|
+
if (
|
|
282
|
+
input.slot < parentBlock.slot &&
|
|
283
|
+
// Don't evict while columns are still being gathered: writeDataColumnsToDb awaits the same
|
|
284
|
+
// hasComputedAllData() before persisting. Such entries are pruned by a later call.
|
|
285
|
+
input.hasComputedAllData() &&
|
|
286
|
+
// Only evict once the payload is FULL (revealed/imported) on the parent's branch. On an EMPTY/PENDING
|
|
287
|
+
// branch we may still need to download the FULL envelope (see #9475), and without the entry gossip
|
|
288
|
+
// for the block is ignored until by-root sync reloads it.
|
|
289
|
+
this.forkChoice.isDescendant(
|
|
290
|
+
input.blockRootHex,
|
|
291
|
+
PayloadStatus.FULL,
|
|
292
|
+
parentBlock.blockRoot,
|
|
293
|
+
parentBlock.payloadStatus
|
|
294
|
+
)
|
|
295
|
+
) {
|
|
296
|
+
this.evictPayloadInput(input, "belowParent");
|
|
279
297
|
}
|
|
280
298
|
}
|
|
281
299
|
}
|
|
@@ -1341,6 +1341,9 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
|
|
|
1341
1341
|
case PayloadErrorCode.ENVELOPE_VERIFICATION_ERROR:
|
|
1342
1342
|
case PayloadErrorCode.EXECUTION_ENGINE_INVALID:
|
|
1343
1343
|
core.reportPeer(peerIdStr, PeerAction.LowToleranceError, "BadGossipPayload");
|
|
1344
|
+
// The builder may have signed another envelope that is valid, keeping this one would make
|
|
1345
|
+
// by-root and range sync reuse it and never import the payload
|
|
1346
|
+
chain.seenPayloadEnvelopeInputCache.removeInvalid(payloadInput);
|
|
1344
1347
|
// Misbehaving peer, but could highlight an issue in another client
|
|
1345
1348
|
logLevel = LogLevel.warn;
|
|
1346
1349
|
break;
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
VoluntaryExitErrorCode,
|
|
21
21
|
} from "../../chain/errors/index.js";
|
|
22
22
|
import {Metrics} from "../../metrics/index.js";
|
|
23
|
+
import {PeerIdStr} from "../../util/peerId.js";
|
|
23
24
|
import {INetworkCore} from "../core/index.js";
|
|
24
25
|
import {
|
|
25
26
|
BatchGossipHandlerFn,
|
|
@@ -40,6 +41,8 @@ export type ValidatorFnModules = {
|
|
|
40
41
|
core: INetworkCore;
|
|
41
42
|
};
|
|
42
43
|
|
|
44
|
+
export type OnFatalPeerFn = (peer: PeerIdStr) => void;
|
|
45
|
+
|
|
43
46
|
type RejectPeerActionRule = {default: PeerAction; byCode?: Record<string, PeerAction>};
|
|
44
47
|
|
|
45
48
|
/**
|
|
@@ -112,7 +115,7 @@ const gossipRejectPeerAction: Record<GossipType, RejectPeerActionRule> = {
|
|
|
112
115
|
[GossipType.light_client_optimistic_update]: {default: PeerAction.HighToleranceError},
|
|
113
116
|
};
|
|
114
117
|
|
|
115
|
-
function rejectPeerAction(type: GossipType, code: string): PeerAction {
|
|
118
|
+
export function rejectPeerAction(type: GossipType, code: string): PeerAction {
|
|
116
119
|
const rule = gossipRejectPeerAction[type];
|
|
117
120
|
return rule.byCode?.[code] ?? rule.default;
|
|
118
121
|
}
|
|
@@ -123,7 +126,8 @@ function rejectPeerAction(type: GossipType, code: string): PeerAction {
|
|
|
123
126
|
*/
|
|
124
127
|
export function getGossipValidatorBatchFn(
|
|
125
128
|
gossipHandlers: GossipHandlers,
|
|
126
|
-
modules: ValidatorFnModules
|
|
129
|
+
modules: ValidatorFnModules,
|
|
130
|
+
onFatalPeer: OnFatalPeerFn
|
|
127
131
|
): GossipValidatorBatchFn {
|
|
128
132
|
const {logger, metrics, core} = modules;
|
|
129
133
|
|
|
@@ -173,6 +177,7 @@ export function getGossipValidatorBatchFn(
|
|
|
173
177
|
metrics?.networkProcessor.gossipAttestationRejectByReason.inc({reason: e.type.code});
|
|
174
178
|
const peerAction = rejectPeerAction(type, e.type.code);
|
|
175
179
|
core.reportPeer(propagationSource, peerAction, e.type.code);
|
|
180
|
+
if (peerAction === PeerAction.Fatal) onFatalPeer(propagationSource);
|
|
176
181
|
logger.debug(
|
|
177
182
|
`Gossip validation ${type} rejected`,
|
|
178
183
|
{peer: propagationSource, clientAgent, clientVersion, peerAction},
|
|
@@ -208,7 +213,11 @@ export function getGossipValidatorBatchFn(
|
|
|
208
213
|
*
|
|
209
214
|
* @see getGossipHandlers for reasoning on why GossipHandlerFn are used for gossip validation.
|
|
210
215
|
*/
|
|
211
|
-
export function getGossipValidatorFn(
|
|
216
|
+
export function getGossipValidatorFn(
|
|
217
|
+
gossipHandlers: GossipHandlers,
|
|
218
|
+
modules: ValidatorFnModules,
|
|
219
|
+
onFatalPeer: OnFatalPeerFn
|
|
220
|
+
): GossipValidatorFn {
|
|
212
221
|
const {logger, metrics, core} = modules;
|
|
213
222
|
|
|
214
223
|
return async function gossipValidatorFn({
|
|
@@ -260,6 +269,7 @@ export function getGossipValidatorFn(gossipHandlers: GossipHandlers, modules: Va
|
|
|
260
269
|
metrics?.networkProcessor.gossipValidationReject.inc({topic: type});
|
|
261
270
|
const peerAction = rejectPeerAction(type, e.type.code);
|
|
262
271
|
core.reportPeer(propagationSource, peerAction, e.type.code);
|
|
272
|
+
if (peerAction === PeerAction.Fatal) onFatalPeer(propagationSource);
|
|
263
273
|
logger.debug(
|
|
264
274
|
`Gossip validation ${type} rejected`,
|
|
265
275
|
{peer: propagationSource, clientAgent, clientVersion, peerAction},
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
getParentRootFromSignedBeaconBlockSerialized,
|
|
23
23
|
getPayloadPresentFromPayloadAttestationMessageSerialized,
|
|
24
24
|
} from "../../util/sszBytes.js";
|
|
25
|
+
import {INetworkCore} from "../core/index.js";
|
|
25
26
|
import {NetworkEvent, NetworkEventBus} from "../events.js";
|
|
26
27
|
import {
|
|
27
28
|
GossipHandlers,
|
|
@@ -34,7 +35,12 @@ import {MAX_PEERS_PER_ROOT} from "./constants.js";
|
|
|
34
35
|
import {createExtractBlockSlotRootFns} from "./extractSlotRootFns.js";
|
|
35
36
|
import {GossipHandlerOpts, ValidatorFnsModules, getGossipHandlers} from "./gossipHandlers.js";
|
|
36
37
|
import {createGossipQueues} from "./gossipQueues/index.js";
|
|
37
|
-
import {
|
|
38
|
+
import {
|
|
39
|
+
ValidatorFnModules,
|
|
40
|
+
getGossipValidatorBatchFn,
|
|
41
|
+
getGossipValidatorFn,
|
|
42
|
+
rejectPeerAction,
|
|
43
|
+
} from "./gossipValidatorFn.js";
|
|
38
44
|
import {PendingGossipsubMessage} from "./types.js";
|
|
39
45
|
|
|
40
46
|
export * from "./types.js";
|
|
@@ -102,6 +108,12 @@ type SearchedRootEntry = {
|
|
|
102
108
|
*/
|
|
103
109
|
const DEFAULT_EARLIEST_PERMISSIBLE_SLOT_DISTANCE = 32;
|
|
104
110
|
|
|
111
|
+
/**
|
|
112
|
+
* The network worker bans a Fatal peer only on its next PeerManager heartbeat (30s). Until then the main thread
|
|
113
|
+
* would keep validating that peer's messages, so drop them here. 3 slots (36s) outlasts one heartbeat.
|
|
114
|
+
*/
|
|
115
|
+
export const FATAL_PEER_EXPIRY_SLOTS = 3;
|
|
116
|
+
|
|
105
117
|
type WorkOpts = {
|
|
106
118
|
bypassQueue?: boolean;
|
|
107
119
|
};
|
|
@@ -232,6 +244,7 @@ type SearchUnknownRootTarget =
|
|
|
232
244
|
*/
|
|
233
245
|
export class NetworkProcessor {
|
|
234
246
|
private readonly chain: IBeaconChain;
|
|
247
|
+
private readonly core: INetworkCore;
|
|
235
248
|
private readonly events: NetworkEventBus;
|
|
236
249
|
private readonly logger: Logger;
|
|
237
250
|
private readonly metrics: Metrics | null;
|
|
@@ -256,23 +269,31 @@ export class NetworkProcessor {
|
|
|
256
269
|
() => new MapDef<RootHex, SearchedRootEntry>(() => ({}))
|
|
257
270
|
);
|
|
258
271
|
private bufferedRootsBySlot = new MapDef<Slot, Set<RootHex>>(() => new Set());
|
|
272
|
+
/** peer -> clock slot when it was Fatal'ed */
|
|
273
|
+
private readonly fatalPeers = new Map<PeerIdStr, Slot>();
|
|
259
274
|
|
|
260
275
|
constructor(
|
|
261
276
|
modules: NetworkProcessorModules,
|
|
262
277
|
private readonly opts: NetworkProcessorOpts
|
|
263
278
|
) {
|
|
264
|
-
const {chain, events, logger, metrics} = modules;
|
|
279
|
+
const {chain, core, events, logger, metrics} = modules;
|
|
265
280
|
this.chain = chain;
|
|
281
|
+
this.core = core;
|
|
266
282
|
this.events = events;
|
|
267
283
|
this.metrics = metrics;
|
|
268
284
|
this.logger = logger;
|
|
269
285
|
this.events = events;
|
|
270
286
|
this.gossipQueues = createGossipQueues();
|
|
271
287
|
this.gossipTopicConcurrency = mapValues(this.gossipQueues, () => 0);
|
|
272
|
-
this.gossipValidatorFn = getGossipValidatorFn(
|
|
288
|
+
this.gossipValidatorFn = getGossipValidatorFn(
|
|
289
|
+
modules.gossipHandlers ?? getGossipHandlers(modules, opts),
|
|
290
|
+
modules,
|
|
291
|
+
this.onFatalPeer
|
|
292
|
+
);
|
|
273
293
|
this.gossipValidatorBatchFn = getGossipValidatorBatchFn(
|
|
274
294
|
modules.gossipHandlers ?? getGossipHandlers(modules, opts),
|
|
275
|
-
modules
|
|
295
|
+
modules,
|
|
296
|
+
this.onFatalPeer
|
|
276
297
|
);
|
|
277
298
|
|
|
278
299
|
events.on(NetworkEvent.pendingGossipsubMessage, this.onPendingGossipsubMessage);
|
|
@@ -406,19 +427,47 @@ export class NetworkProcessor {
|
|
|
406
427
|
}
|
|
407
428
|
}
|
|
408
429
|
|
|
430
|
+
private onFatalPeer = (peer: PeerIdStr): void => {
|
|
431
|
+
this.fatalPeers.set(peer, this.chain.clock.currentSlot);
|
|
432
|
+
};
|
|
433
|
+
|
|
409
434
|
private onPendingGossipsubMessage = (message: PendingGossipsubMessage): void => {
|
|
410
435
|
const topicType = message.topic.type;
|
|
436
|
+
if (this.fatalPeers.has(message.propagationSource)) {
|
|
437
|
+
this.metrics?.networkProcessor.gossipValidationError.inc({topic: topicType, error: GossipErrorCode.FATAL_PEER});
|
|
438
|
+
// No need to report the dropped job to gossip. It will be eventually pruned from the mcache
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
|
|
411
442
|
const extractBlockSlotRootFn = this.extractBlockSlotRootFns[topicType];
|
|
443
|
+
if (extractBlockSlotRootFn === undefined) {
|
|
444
|
+
// some messages don't have slot and root
|
|
445
|
+
this.pushPendingGossipsubMessageToQueue(message);
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
412
448
|
|
|
413
449
|
// 1st extract round: make sure slot is in range and if block root is not available
|
|
414
450
|
// proactively search for it + queue the message
|
|
415
|
-
const slotRoot = extractBlockSlotRootFn
|
|
416
|
-
? extractBlockSlotRootFn(message.msg.data, message.topic.boundary.fork)
|
|
417
|
-
: null;
|
|
451
|
+
const slotRoot = extractBlockSlotRootFn(message.msg.data, message.topic.boundary.fork);
|
|
418
452
|
if (slotRoot === null) {
|
|
419
|
-
//
|
|
420
|
-
|
|
421
|
-
|
|
453
|
+
// DOS protection: null means malformed bytes or slot >= 2^32, neither is forwarded by honest peers.
|
|
454
|
+
const {propagationSource, clientAgent, clientVersion} = message;
|
|
455
|
+
const code = GossipErrorCode.INVALID_SLOT;
|
|
456
|
+
const peerAction = rejectPeerAction(topicType, code);
|
|
457
|
+
this.metrics?.networkProcessor.gossipValidationReject.inc({topic: topicType});
|
|
458
|
+
this.core.reportPeer(propagationSource, peerAction, code);
|
|
459
|
+
this.logger.debug(`Gossip validation ${topicType} rejected due to null slot`, {
|
|
460
|
+
peer: propagationSource,
|
|
461
|
+
clientAgent,
|
|
462
|
+
clientVersion,
|
|
463
|
+
peerAction,
|
|
464
|
+
code,
|
|
465
|
+
});
|
|
466
|
+
this.events.emit(NetworkEvent.gossipMessageValidationResult, {
|
|
467
|
+
msgId: message.msgId,
|
|
468
|
+
propagationSource,
|
|
469
|
+
acceptance: TopicValidatorResult.Reject,
|
|
470
|
+
});
|
|
422
471
|
return;
|
|
423
472
|
}
|
|
424
473
|
|
|
@@ -872,6 +921,10 @@ export class NetworkProcessor {
|
|
|
872
921
|
for (const slot of this.bufferedRootsBySlot.keys()) {
|
|
873
922
|
if (slot <= minSlot) this.bufferedRootsBySlot.delete(slot);
|
|
874
923
|
}
|
|
924
|
+
|
|
925
|
+
for (const [peer, slot] of this.fatalPeers) {
|
|
926
|
+
if (clockSlot - slot > FATAL_PEER_EXPIRY_SLOTS) this.fatalPeers.delete(peer);
|
|
927
|
+
}
|
|
875
928
|
};
|
|
876
929
|
|
|
877
930
|
private executeWork(): void {
|
|
@@ -927,6 +980,32 @@ export class NetworkProcessor {
|
|
|
927
980
|
private async processPendingGossipsubMessage(
|
|
928
981
|
messageOrArray: PendingGossipsubMessage | PendingGossipsubMessage[]
|
|
929
982
|
): Promise<void> {
|
|
983
|
+
// drop messages from peers Fatal'ed after these were queued or started awaiting a block/envelope
|
|
984
|
+
// No need to report the dropped jobs to gossip. They will be eventually pruned from the mcache
|
|
985
|
+
if (this.fatalPeers.size > 0) {
|
|
986
|
+
if (Array.isArray(messageOrArray)) {
|
|
987
|
+
const allowed: PendingGossipsubMessage[] = [];
|
|
988
|
+
for (const msg of messageOrArray) {
|
|
989
|
+
if (this.fatalPeers.has(msg.propagationSource)) {
|
|
990
|
+
this.metrics?.networkProcessor.gossipValidationError.inc({
|
|
991
|
+
topic: msg.topic.type,
|
|
992
|
+
error: GossipErrorCode.FATAL_PEER,
|
|
993
|
+
});
|
|
994
|
+
} else {
|
|
995
|
+
allowed.push(msg);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
if (allowed.length === 0) return;
|
|
999
|
+
messageOrArray = allowed;
|
|
1000
|
+
} else if (this.fatalPeers.has(messageOrArray.propagationSource)) {
|
|
1001
|
+
this.metrics?.networkProcessor.gossipValidationError.inc({
|
|
1002
|
+
topic: messageOrArray.topic.type,
|
|
1003
|
+
error: GossipErrorCode.FATAL_PEER,
|
|
1004
|
+
});
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
|
|
930
1009
|
const nowSec = Date.now() / 1000;
|
|
931
1010
|
if (Array.isArray(messageOrArray)) {
|
|
932
1011
|
for (const msg of messageOrArray) {
|
|
@@ -53,7 +53,7 @@ export async function* onExecutionPayloadEnvelopesByRoot(
|
|
|
53
53
|
const batch = requests.slice(i, i + MAX_BODIES_PER_REQUEST);
|
|
54
54
|
let envelopesBytes: (Uint8Array | null)[];
|
|
55
55
|
try {
|
|
56
|
-
// By-root allows omission, so
|
|
56
|
+
// By-root allows omission, so an envelope that cannot be rebuilt is left out rather than failing the response.
|
|
57
57
|
envelopesBytes = await chain.getSerializedExecutionPayloadEnvelopes(batch, "omit");
|
|
58
58
|
} catch (e) {
|
|
59
59
|
if (e instanceof EnvelopeReconstructionError) {
|
package/src/sync/range/range.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {IBeaconStateViewGloas, computeStartSlotAtEpoch, isStatePostGloas} from "
|
|
|
5
5
|
import {Epoch, Status, fulu} from "@lodestar/types";
|
|
6
6
|
import {Logger, prettyPrintIndices, toRootHex} from "@lodestar/utils";
|
|
7
7
|
import {IBlockInput} from "../../chain/blocks/blockInput/types.js";
|
|
8
|
+
import {PayloadError, PayloadErrorCode} from "../../chain/blocks/importExecutionPayload.js";
|
|
8
9
|
import {AttestationImportOpt, ImportBlockOpts} from "../../chain/blocks/index.js";
|
|
9
10
|
import {assertLinearChainSegment} from "../../chain/blocks/utils/chainSegment.js";
|
|
10
11
|
import {BlockError} from "../../chain/errors/index.js";
|
|
@@ -207,22 +208,50 @@ export class RangeSync extends (EventEmitter as {new (): RangeSyncEmitter}) {
|
|
|
207
208
|
blsVerifyOnMainThread: false,
|
|
208
209
|
};
|
|
209
210
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
211
|
+
try {
|
|
212
|
+
if (this.opts?.disableProcessAsChainSegment) {
|
|
213
|
+
// Should only be used for debugging or testing
|
|
214
|
+
for (const block of blocks) {
|
|
215
|
+
await this.chain.processBlock(block, flags);
|
|
216
|
+
const payloadEnvelope = payloadEnvelopes?.get(block.slot);
|
|
217
|
+
if (payloadEnvelope?.hasPayloadEnvelope()) {
|
|
218
|
+
await this.chain.processExecutionPayload(payloadEnvelope);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
} else {
|
|
222
|
+
const {orphaned, skipped} = await this.chain.processChainSegment(blocks, payloadEnvelopes, flags);
|
|
223
|
+
// We log orphaned payloads to work with different clients
|
|
224
|
+
// in the future, consider applying penalties in certain conditions
|
|
225
|
+
// make sure the payload source was from range sync in that case
|
|
226
|
+
for (const {slot, payloadEnvelopeInput} of orphaned) {
|
|
227
|
+
const {peerIdStr, source} = payloadEnvelopeInput.getPayloadEnvelopeSource();
|
|
228
|
+
let client = "unknown";
|
|
229
|
+
if (peerIdStr !== undefined) {
|
|
230
|
+
try {
|
|
231
|
+
client = this.getConnectedPeerSyncMeta(peerIdStr).client;
|
|
232
|
+
} catch {
|
|
233
|
+
// peer disconnected since serving the envelope, keep "unknown"
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
this.logger.debug("Orphaned payload envelope in range sync batch", {
|
|
237
|
+
slot,
|
|
238
|
+
root: payloadEnvelopeInput.blockRootHex,
|
|
239
|
+
peer: peerIdStr ?? "unknown",
|
|
240
|
+
source,
|
|
241
|
+
client,
|
|
242
|
+
skipped,
|
|
243
|
+
});
|
|
217
244
|
}
|
|
218
245
|
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
246
|
+
} catch (e) {
|
|
247
|
+
if (
|
|
248
|
+
e instanceof PayloadError &&
|
|
249
|
+
(e.type.code === PayloadErrorCode.INVALID_SIGNATURE ||
|
|
250
|
+
e.type.code === PayloadErrorCode.ENVELOPE_VERIFICATION_ERROR ||
|
|
251
|
+
e.type.code === PayloadErrorCode.EXECUTION_ENGINE_INVALID)
|
|
252
|
+
) {
|
|
253
|
+
const {payloadInput} = e;
|
|
254
|
+
const {peerIdStr, source} = payloadInput.getPayloadEnvelopeSource();
|
|
226
255
|
let client = "unknown";
|
|
227
256
|
if (peerIdStr !== undefined) {
|
|
228
257
|
try {
|
|
@@ -231,15 +260,18 @@ export class RangeSync extends (EventEmitter as {new (): RangeSyncEmitter}) {
|
|
|
231
260
|
// peer disconnected since serving the envelope, keep "unknown"
|
|
232
261
|
}
|
|
233
262
|
}
|
|
234
|
-
this.logger.debug("
|
|
235
|
-
slot,
|
|
236
|
-
root:
|
|
263
|
+
this.logger.debug("Removing invalid payload envelope in range sync batch", {
|
|
264
|
+
slot: payloadInput.slot,
|
|
265
|
+
root: payloadInput.blockRootHex,
|
|
237
266
|
peer: peerIdStr ?? "unknown",
|
|
238
267
|
source,
|
|
239
268
|
client,
|
|
240
|
-
|
|
269
|
+
code: e.type.code,
|
|
241
270
|
});
|
|
271
|
+
// The batch is downloaded again, it would otherwise reuse the cached envelope and fail the same way
|
|
272
|
+
this.chain.seenPayloadEnvelopeInputCache.removeInvalid(payloadInput);
|
|
242
273
|
}
|
|
274
|
+
throw e;
|
|
243
275
|
}
|
|
244
276
|
};
|
|
245
277
|
|
package/src/sync/unknownBlock.ts
CHANGED
|
@@ -1426,20 +1426,23 @@ export class BlockInputSync {
|
|
|
1426
1426
|
pendingPayload.status = PendingPayloadInputStatus.downloaded;
|
|
1427
1427
|
break;
|
|
1428
1428
|
|
|
1429
|
+
// The invalid envelope is evicted from the seen cache in the cases below, otherwise the next
|
|
1430
|
+
// attempt would reuse it instead of fetching another envelope from peers
|
|
1429
1431
|
case PayloadErrorCode.EXECUTION_ENGINE_INVALID:
|
|
1430
1432
|
this.logger.debug("Error processing payload from unknown sync", logCtx, res.err);
|
|
1433
|
+
this.chain.seenPayloadEnvelopeInputCache.removeInvalid(pendingPayload.payloadInput);
|
|
1431
1434
|
this.removePendingPayloadAndDescendants(rootHex, DroppedItemReason.elInvalid);
|
|
1432
1435
|
break;
|
|
1433
1436
|
|
|
1434
1437
|
case PayloadErrorCode.ENVELOPE_VERIFICATION_ERROR:
|
|
1435
1438
|
this.logger.debug("Error processing payload from unknown sync", logCtx, res.err);
|
|
1439
|
+
this.chain.seenPayloadEnvelopeInputCache.removeInvalid(pendingPayload.payloadInput);
|
|
1436
1440
|
this.removePendingPayloadAndDescendants(rootHex, DroppedItemReason.invalidEnvelope);
|
|
1437
1441
|
break;
|
|
1438
1442
|
|
|
1439
1443
|
case PayloadErrorCode.INVALID_SIGNATURE:
|
|
1440
|
-
// TODO GLOAS: Decide how invalid payload inputs should eventually leave memory without
|
|
1441
|
-
// reintroducing envelope replacement / recreation flows.
|
|
1442
1444
|
this.logger.debug("Error processing payload from unknown sync", logCtx, res.err);
|
|
1445
|
+
this.chain.seenPayloadEnvelopeInputCache.removeInvalid(pendingPayload.payloadInput);
|
|
1443
1446
|
this.removePendingPayloadAndDescendants(rootHex, DroppedItemReason.invalidSignature);
|
|
1444
1447
|
break;
|
|
1445
1448
|
|
|
@@ -1879,8 +1882,6 @@ export class BlockInputSync {
|
|
|
1879
1882
|
|
|
1880
1883
|
// Once a parent payload is invalid, every descendant waiting on that payload lineage becomes unrecoverable too.
|
|
1881
1884
|
private removePendingPayloadAndDescendants(rootHex: RootHex, headReason: DroppedItemReason): void {
|
|
1882
|
-
// Keep PayloadEnvelopeInput resident in the seen cache. importBlock() owns that object and
|
|
1883
|
-
// later validation/finalization logic decides when it can leave memory.
|
|
1884
1885
|
if (this.pendingPayloads.delete(rootHex)) {
|
|
1885
1886
|
this.metrics?.blockInputSync.removedPayloads.inc({reason: headReason}, 1);
|
|
1886
1887
|
}
|