@lodestar/beacon-node 1.50.0-dev.2e668c516f → 1.50.0-dev.3bd495295e

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/lib/chain/blocks/importBlock.d.ts.map +1 -1
  2. package/lib/chain/blocks/importBlock.js +4 -0
  3. package/lib/chain/blocks/importBlock.js.map +1 -1
  4. package/lib/chain/blocks/payloadEnvelopeInput/types.d.ts +2 -1
  5. package/lib/chain/blocks/payloadEnvelopeInput/types.d.ts.map +1 -1
  6. package/lib/chain/errors/gossipValidation.d.ts +2 -1
  7. package/lib/chain/errors/gossipValidation.d.ts.map +1 -1
  8. package/lib/chain/errors/gossipValidation.js +1 -0
  9. package/lib/chain/errors/gossipValidation.js.map +1 -1
  10. package/lib/chain/prepareNextSlot.d.ts.map +1 -1
  11. package/lib/chain/prepareNextSlot.js +5 -2
  12. package/lib/chain/prepareNextSlot.js.map +1 -1
  13. package/lib/chain/produceBlock/produceBlockBody.d.ts +3 -1
  14. package/lib/chain/produceBlock/produceBlockBody.d.ts.map +1 -1
  15. package/lib/chain/produceBlock/produceBlockBody.js +6 -2
  16. package/lib/chain/produceBlock/produceBlockBody.js.map +1 -1
  17. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts +6 -0
  18. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts.map +1 -1
  19. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js +10 -0
  20. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js.map +1 -1
  21. package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
  22. package/lib/network/processor/gossipHandlers.js +3 -0
  23. package/lib/network/processor/gossipHandlers.js.map +1 -1
  24. package/lib/network/processor/gossipValidatorFn.d.ts +3 -1
  25. package/lib/network/processor/gossipValidatorFn.d.ts.map +1 -1
  26. package/lib/network/processor/gossipValidatorFn.js +1 -1
  27. package/lib/network/processor/gossipValidatorFn.js.map +1 -1
  28. package/lib/network/processor/index.d.ts +1 -0
  29. package/lib/network/processor/index.d.ts.map +1 -1
  30. package/lib/network/processor/index.js +28 -8
  31. package/lib/network/processor/index.js.map +1 -1
  32. package/lib/sync/range/range.d.ts.map +1 -1
  33. package/lib/sync/range/range.js +50 -18
  34. package/lib/sync/range/range.js.map +1 -1
  35. package/lib/sync/unknownBlock.d.ts.map +1 -1
  36. package/lib/sync/unknownBlock.js +5 -4
  37. package/lib/sync/unknownBlock.js.map +1 -1
  38. package/package.json +15 -15
  39. package/src/chain/blocks/importBlock.ts +4 -0
  40. package/src/chain/blocks/payloadEnvelopeInput/types.ts +2 -1
  41. package/src/chain/errors/gossipValidation.ts +1 -0
  42. package/src/chain/prepareNextSlot.ts +6 -3
  43. package/src/chain/produceBlock/produceBlockBody.ts +8 -1
  44. package/src/chain/seenCache/seenPayloadEnvelopeInput.ts +11 -0
  45. package/src/network/processor/gossipHandlers.ts +3 -0
  46. package/src/network/processor/gossipValidatorFn.ts +1 -1
  47. package/src/network/processor/index.ts +34 -8
  48. package/src/sync/range/range.ts +50 -18
  49. package/src/sync/unknownBlock.ts +5 -4
@@ -22,8 +22,9 @@ export enum PayloadEnvelopeInputSource {
22
22
  * - finalized: below the finalized slot
23
23
  * - prune: explicit prune by root
24
24
  * - cap: insertion-order backstop cap (MAX_PAYLOAD_ENVELOPE_INPUT_CACHE_SIZE)
25
+ * - invalid: the cached envelope failed import with a definitive invalid verdict
25
26
  */
26
- export type PayloadEnvelopeInputPruneReason = "belowParent" | "finalized" | "remove" | "cap";
27
+ export type PayloadEnvelopeInputPruneReason = "belowParent" | "finalized" | "remove" | "cap" | "invalid";
27
28
 
28
29
  export type SourceMeta = {
29
30
  source: PayloadEnvelopeInputSource;
@@ -9,6 +9,7 @@ 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
+ INVALID_SLOT = "GOSSIP_ERROR_INVALID_SLOT",
12
13
  }
13
14
 
14
15
  export class GossipActionError<T extends {code: string}> extends LodestarError<T> {
@@ -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 (!isForkPostGloas(fork)) {
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
  }
@@ -264,6 +264,17 @@ export class SeenPayloadEnvelopeInput {
264
264
  }
265
265
  }
266
266
 
267
+ /**
268
+ * Removes a PayloadEnvelopeInput whose envelope failed import with a definitive invalid verdict. The envelope
269
+ * cannot be replaced in place, sync recreates the entry via `getOrReload()` or `add()` to fetch another one.
270
+ * No-op if the cached entry is a different object, it may already be the recreated one.
271
+ */
272
+ removeInvalid(payloadInput: PayloadEnvelopeInput): void {
273
+ if (this.payloadInputs.get(payloadInput.blockRootHex) === payloadInput) {
274
+ this.evictPayloadInput(payloadInput, "invalid");
275
+ }
276
+ }
277
+
267
278
  pruneBelowParent(parentBlock: ProtoBlock): void {
268
279
  for (const block of this.forkChoice.getAllAncestorBlocks(parentBlock.blockRoot, parentBlock.payloadStatus)) {
269
280
  // Only evict once the payload is FULL (revealed/imported) — on an EMPTY/PENDING branch we may
@@ -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;
@@ -112,7 +112,7 @@ const gossipRejectPeerAction: Record<GossipType, RejectPeerActionRule> = {
112
112
  [GossipType.light_client_optimistic_update]: {default: PeerAction.HighToleranceError},
113
113
  };
114
114
 
115
- function rejectPeerAction(type: GossipType, code: string): PeerAction {
115
+ export function rejectPeerAction(type: GossipType, code: string): PeerAction {
116
116
  const rule = gossipRejectPeerAction[type];
117
117
  return rule.byCode?.[code] ?? rule.default;
118
118
  }
@@ -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 {ValidatorFnModules, getGossipValidatorBatchFn, getGossipValidatorFn} from "./gossipValidatorFn.js";
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";
@@ -232,6 +238,7 @@ type SearchUnknownRootTarget =
232
238
  */
233
239
  export class NetworkProcessor {
234
240
  private readonly chain: IBeaconChain;
241
+ private readonly core: INetworkCore;
235
242
  private readonly events: NetworkEventBus;
236
243
  private readonly logger: Logger;
237
244
  private readonly metrics: Metrics | null;
@@ -261,8 +268,9 @@ export class NetworkProcessor {
261
268
  modules: NetworkProcessorModules,
262
269
  private readonly opts: NetworkProcessorOpts
263
270
  ) {
264
- const {chain, events, logger, metrics} = modules;
271
+ const {chain, core, events, logger, metrics} = modules;
265
272
  this.chain = chain;
273
+ this.core = core;
266
274
  this.events = events;
267
275
  this.metrics = metrics;
268
276
  this.logger = logger;
@@ -409,16 +417,34 @@ export class NetworkProcessor {
409
417
  private onPendingGossipsubMessage = (message: PendingGossipsubMessage): void => {
410
418
  const topicType = message.topic.type;
411
419
  const extractBlockSlotRootFn = this.extractBlockSlotRootFns[topicType];
420
+ if (extractBlockSlotRootFn === undefined) {
421
+ // some messages don't have slot and root
422
+ this.pushPendingGossipsubMessageToQueue(message);
423
+ return;
424
+ }
412
425
 
413
426
  // 1st extract round: make sure slot is in range and if block root is not available
414
427
  // proactively search for it + queue the message
415
- const slotRoot = extractBlockSlotRootFn
416
- ? extractBlockSlotRootFn(message.msg.data, message.topic.boundary.fork)
417
- : null;
428
+ const slotRoot = extractBlockSlotRootFn(message.msg.data, message.topic.boundary.fork);
418
429
  if (slotRoot === null) {
419
- // some messages don't have slot and root
420
- // if the msg.data is invalid, message will be rejected when deserializing data in later phase (gossipValidatorFn)
421
- this.pushPendingGossipsubMessageToQueue(message);
430
+ // DOS protection: null means malformed bytes or slot >= 2^32, neither is forwarded by honest peers.
431
+ const {propagationSource, clientAgent, clientVersion} = message;
432
+ const code = GossipErrorCode.INVALID_SLOT;
433
+ const peerAction = rejectPeerAction(topicType, code);
434
+ this.metrics?.networkProcessor.gossipValidationReject.inc({topic: topicType});
435
+ this.core.reportPeer(propagationSource, peerAction, code);
436
+ this.logger.debug(`Gossip validation ${topicType} rejected due to null slot`, {
437
+ peer: propagationSource,
438
+ clientAgent,
439
+ clientVersion,
440
+ peerAction,
441
+ code,
442
+ });
443
+ this.events.emit(NetworkEvent.gossipMessageValidationResult, {
444
+ msgId: message.msgId,
445
+ propagationSource,
446
+ acceptance: TopicValidatorResult.Reject,
447
+ });
422
448
  return;
423
449
  }
424
450
 
@@ -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
- if (this.opts?.disableProcessAsChainSegment) {
211
- // Should only be used for debugging or testing
212
- for (const block of blocks) {
213
- await this.chain.processBlock(block, flags);
214
- const payloadEnvelope = payloadEnvelopes?.get(block.slot);
215
- if (payloadEnvelope?.hasPayloadEnvelope()) {
216
- await this.chain.processExecutionPayload(payloadEnvelope);
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
- } else {
220
- const {orphaned, skipped} = await this.chain.processChainSegment(blocks, payloadEnvelopes, flags);
221
- // We log orphaned payloads to work with different clients
222
- // in the future, consider applying penalties in certain conditions
223
- // make sure the payload source was from range sync in that case
224
- for (const {slot, payloadEnvelopeInput} of orphaned) {
225
- const {peerIdStr, source} = payloadEnvelopeInput.getPayloadEnvelopeSource();
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("Orphaned payload envelope in range sync batch", {
235
- slot,
236
- root: payloadEnvelopeInput.blockRootHex,
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
- skipped,
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
 
@@ -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
  }