@lodestar/beacon-node 1.49.0-dev.72d74345fb → 1.49.0-dev.73ea27a093

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 (61) hide show
  1. package/lib/api/impl/validator/index.d.ts.map +1 -1
  2. package/lib/api/impl/validator/index.js +3 -1
  3. package/lib/api/impl/validator/index.js.map +1 -1
  4. package/lib/chain/archiveStore/utils/pruneHistory.d.ts.map +1 -1
  5. package/lib/chain/archiveStore/utils/pruneHistory.js +10 -2
  6. package/lib/chain/archiveStore/utils/pruneHistory.js.map +1 -1
  7. package/lib/chain/emitter.d.ts +10 -0
  8. package/lib/chain/emitter.d.ts.map +1 -1
  9. package/lib/chain/emitter.js +4 -0
  10. package/lib/chain/emitter.js.map +1 -1
  11. package/lib/chain/errors/attesterSlashingError.d.ts +4 -1
  12. package/lib/chain/errors/attesterSlashingError.d.ts.map +1 -1
  13. package/lib/chain/errors/attesterSlashingError.js +1 -0
  14. package/lib/chain/errors/attesterSlashingError.js.map +1 -1
  15. package/lib/chain/errors/proposerSlashingError.d.ts +4 -1
  16. package/lib/chain/errors/proposerSlashingError.d.ts.map +1 -1
  17. package/lib/chain/errors/proposerSlashingError.js +1 -0
  18. package/lib/chain/errors/proposerSlashingError.js.map +1 -1
  19. package/lib/chain/validation/attesterSlashing.d.ts.map +1 -1
  20. package/lib/chain/validation/attesterSlashing.js +1 -2
  21. package/lib/chain/validation/attesterSlashing.js.map +1 -1
  22. package/lib/chain/validation/proposerSlashing.js +1 -2
  23. package/lib/chain/validation/proposerSlashing.js.map +1 -1
  24. package/lib/db/repositories/blockArchive.d.ts +5 -0
  25. package/lib/db/repositories/blockArchive.d.ts.map +1 -1
  26. package/lib/db/repositories/blockArchive.js +36 -1
  27. package/lib/db/repositories/blockArchive.js.map +1 -1
  28. package/lib/network/gossip/topic.d.ts +1 -1
  29. package/lib/network/gossip/topic.d.ts.map +1 -1
  30. package/lib/network/gossip/topic.js +1 -1
  31. package/lib/network/gossip/topic.js.map +1 -1
  32. package/lib/network/processor/gossipValidatorFn.d.ts +2 -0
  33. package/lib/network/processor/gossipValidatorFn.d.ts.map +1 -1
  34. package/lib/network/processor/gossipValidatorFn.js +88 -7
  35. package/lib/network/processor/gossipValidatorFn.js.map +1 -1
  36. package/lib/network/processor/index.d.ts +6 -2
  37. package/lib/network/processor/index.d.ts.map +1 -1
  38. package/lib/network/processor/index.js +26 -9
  39. package/lib/network/processor/index.js.map +1 -1
  40. package/lib/sync/types.d.ts +4 -0
  41. package/lib/sync/types.d.ts.map +1 -1
  42. package/lib/sync/types.js +4 -0
  43. package/lib/sync/types.js.map +1 -1
  44. package/lib/sync/unknownBlock.d.ts +1 -0
  45. package/lib/sync/unknownBlock.d.ts.map +1 -1
  46. package/lib/sync/unknownBlock.js +15 -0
  47. package/lib/sync/unknownBlock.js.map +1 -1
  48. package/package.json +14 -14
  49. package/src/api/impl/validator/index.ts +3 -1
  50. package/src/chain/archiveStore/utils/pruneHistory.ts +10 -3
  51. package/src/chain/emitter.ts +7 -2
  52. package/src/chain/errors/attesterSlashingError.ts +3 -1
  53. package/src/chain/errors/proposerSlashingError.ts +3 -1
  54. package/src/chain/validation/attesterSlashing.ts +1 -2
  55. package/src/chain/validation/proposerSlashing.ts +1 -2
  56. package/src/db/repositories/blockArchive.ts +38 -1
  57. package/src/network/gossip/topic.ts +2 -2
  58. package/src/network/processor/gossipValidatorFn.ts +111 -7
  59. package/src/network/processor/index.ts +37 -9
  60. package/src/sync/types.ts +6 -3
  61. package/src/sync/unknownBlock.ts +15 -0
@@ -1057,6 +1057,8 @@ export function getValidatorApi(
1057
1057
  rank: index + 1,
1058
1058
  source: candidate.url !== undefined ? toPrintableUrl(candidate.url) : "p2p",
1059
1059
  builder: candidate.signedBid.message.builderIndex,
1060
+ value: prettyGweiToEth(candidate.signedBid.message.value),
1061
+ executionPayment: prettyGweiToEth(candidate.signedBid.message.executionPayment),
1060
1062
  total: prettyGweiToEth(candidate.totalGwei),
1061
1063
  boost: candidate.boostFactor,
1062
1064
  boosted: prettyGweiToEth(getBoostedTotalScaled(candidate) / 100n),
@@ -2104,7 +2106,7 @@ export function getValidatorApi(
2104
2106
  auth: entry.auth,
2105
2107
  });
2106
2108
  } catch (e) {
2107
- failures.push({index: i, message: (e as Error).message});
2109
+ failures.push({index: i, message: `${builder}: ${(e as Error).message}`});
2108
2110
  logger.verbose(
2109
2111
  `Error on submitBuilderPreferences [${i}]`,
2110
2112
  {slot: entry.auth.message.slot, builder},
@@ -1,7 +1,7 @@
1
1
  import {ChainConfig} from "@lodestar/config";
2
2
  import {computeStartSlotAtEpoch} from "@lodestar/state-transition";
3
3
  import {Epoch} from "@lodestar/types";
4
- import {Logger} from "@lodestar/utils";
4
+ import {Logger, prettyPrintIndices} from "@lodestar/utils";
5
5
  import {IBeaconDb} from "../../../db/interface.js";
6
6
  import {Metrics} from "../../../metrics/index.js";
7
7
 
@@ -20,18 +20,22 @@ export async function pruneHistory(
20
20
  finalizedEpoch
21
21
  );
22
22
  const blockCutoffSlot = computeStartSlotAtEpoch(blockCutoffEpoch);
23
+ // The latest archived state is the anchor on restart and can trail finalization by a few epochs
24
+ const lastArchivedStateSlot = (await db.stateArchive.lastKey()) ?? 0;
25
+ const stateCutoffSlot = Math.min(computeStartSlotAtEpoch(finalizedEpoch), lastArchivedStateSlot);
23
26
 
24
27
  logger.debug("Preparing to prune history", {
25
28
  currentEpoch,
26
29
  finalizedEpoch,
27
30
  blockCutoffEpoch,
31
+ stateCutoffSlot,
28
32
  });
29
33
 
30
34
  const step0 = metrics?.pruneHistory.fetchKeys.startTimer();
31
35
  const [blocks, envelopes, states] = await Promise.all([
32
36
  db.blockArchive.keys({gte: 0, lt: blockCutoffSlot}),
33
37
  db.executionPayloadEnvelopeArchive.keys({gte: 0, lt: blockCutoffSlot}),
34
- db.stateArchive.keys({gte: 0, lt: finalizedEpoch}),
38
+ db.stateArchive.keys({gte: 0, lt: stateCutoffSlot}),
35
39
  ]);
36
40
  step0?.();
37
41
 
@@ -40,12 +44,15 @@ export async function pruneHistory(
40
44
  blocksToPrune: blocks.length,
41
45
  envelopesToPrune: envelopes.length,
42
46
  statesToPrune: states.length,
47
+ blockSlots: prettyPrintIndices(blocks),
48
+ envelopeSlots: prettyPrintIndices(envelopes),
49
+ stateSlots: prettyPrintIndices(states),
43
50
  });
44
51
 
45
52
  const step1 = metrics?.pruneHistory.pruneKeys.startTimer();
46
53
  await Promise.all([
47
54
  // ->
48
- db.blockArchive.batchDelete(blocks),
55
+ db.blockArchive.batchDeleteRange(blocks),
49
56
  db.executionPayloadEnvelopeArchive.batchDelete(envelopes),
50
57
  db.stateArchive.batchDelete(states),
51
58
  ]);
@@ -72,6 +72,10 @@ export enum ChainEvent {
72
72
  * Trigger BlockInputSync to find a SignedExecutionPayloadEnvelope with specified block root.
73
73
  */
74
74
  unknownEnvelopeBlockRoot = "unknownEnvelopeBlockRoot",
75
+ /**
76
+ * Same as unknownEnvelopeBlockRoot, but with slot
77
+ */
78
+ unknownEnvelopeBlockRootSlot = "unknownEnvelopeBlockRootSlot",
75
79
  /**
76
80
  * Trigger BlockInputSync for blocks that are partially received via gossip but are not complete by time the
77
81
  * cut-off window passes for waiting on gossip
@@ -99,8 +103,8 @@ export type ChainEventData = {
99
103
  peer: PeerIdStr;
100
104
  source: BlockInputSource;
101
105
  };
102
- // slot is the message slot, not necessarily the envelope's slot, but useful as a logging/prune hint
103
- [ChainEvent.unknownEnvelopeBlockRoot]: {rootHex: RootHex; slot: Slot; peer?: PeerIdStr; source: BlockInputSource};
106
+ [ChainEvent.unknownEnvelopeBlockRoot]: {rootHex: RootHex; peer?: PeerIdStr; source: BlockInputSource};
107
+ [ChainEvent.unknownEnvelopeBlockRootSlot]: {rootHex: RootHex; slot: Slot; peer?: PeerIdStr; source: BlockInputSource};
104
108
  };
105
109
 
106
110
  export type IChainEvents = ApiEvents & {
@@ -126,6 +130,7 @@ export type IChainEvents = ApiEvents & {
126
130
  [ChainEvent.incompleteBlockInput]: (data: ChainEventData[ChainEvent.incompleteBlockInput]) => void;
127
131
  [ChainEvent.incompletePayloadEnvelope]: (data: ChainEventData[ChainEvent.incompletePayloadEnvelope]) => void;
128
132
  [ChainEvent.unknownEnvelopeBlockRoot]: (data: ChainEventData[ChainEvent.unknownEnvelopeBlockRoot]) => void;
133
+ [ChainEvent.unknownEnvelopeBlockRootSlot]: (data: ChainEventData[ChainEvent.unknownEnvelopeBlockRootSlot]) => void;
129
134
  };
130
135
 
131
136
  /**
@@ -3,9 +3,11 @@ import {GossipActionError} from "./gossipValidation.js";
3
3
  export enum AttesterSlashingErrorCode {
4
4
  ALREADY_EXISTS = "ATTESTATION_SLASHING_ERROR_ALREADY_EXISTS",
5
5
  INVALID = "ATTESTATION_SLASHING_ERROR_INVALID",
6
+ INVALID_SIGNATURE = "ATTESTATION_SLASHING_ERROR_INVALID_SIGNATURE",
6
7
  }
7
8
  export type AttesterSlashingErrorType =
8
9
  | {code: AttesterSlashingErrorCode.ALREADY_EXISTS}
9
- | {code: AttesterSlashingErrorCode.INVALID; error: Error};
10
+ | {code: AttesterSlashingErrorCode.INVALID; error: Error}
11
+ | {code: AttesterSlashingErrorCode.INVALID_SIGNATURE};
10
12
 
11
13
  export class AttesterSlashingError extends GossipActionError<AttesterSlashingErrorType> {}
@@ -3,9 +3,11 @@ import {GossipActionError} from "./gossipValidation.js";
3
3
  export enum ProposerSlashingErrorCode {
4
4
  ALREADY_EXISTS = "PROPOSER_SLASHING_ERROR_ALREADY_EXISTS",
5
5
  INVALID = "PROPOSER_SLASHING_ERROR_INVALID",
6
+ INVALID_SIGNATURE = "PROPOSER_SLASHING_ERROR_INVALID_SIGNATURE",
6
7
  }
7
8
  export type ProposerSlashingErrorType =
8
9
  | {code: ProposerSlashingErrorCode.ALREADY_EXISTS}
9
- | {code: ProposerSlashingErrorCode.INVALID; error: Error};
10
+ | {code: ProposerSlashingErrorCode.INVALID; error: Error}
11
+ | {code: ProposerSlashingErrorCode.INVALID_SIGNATURE};
10
12
 
11
13
  export class ProposerSlashingError extends GossipActionError<ProposerSlashingErrorType> {}
@@ -63,8 +63,7 @@ export async function validateAttesterSlashing(
63
63
  const signatureSets = getAttesterSlashingSignatureSets(chain.config, state.slot, attesterSlashing);
64
64
  if (!(await chain.bls.verifySignatureSets(signatureSets, {batchable: true, priority: prioritizeBls}))) {
65
65
  throw new AttesterSlashingError(GossipAction.REJECT, {
66
- code: AttesterSlashingErrorCode.INVALID,
67
- error: Error("Invalid signature"),
66
+ code: AttesterSlashingErrorCode.INVALID_SIGNATURE,
68
67
  });
69
68
  }
70
69
  }
@@ -48,8 +48,7 @@ async function validateProposerSlashing(
48
48
  const signatureSets = getProposerSlashingSignatureSets(chain.config, state.slot, proposerSlashing);
49
49
  if (!(await chain.bls.verifySignatureSets(signatureSets, {batchable: true, priority: prioritizeBls}))) {
50
50
  throw new ProposerSlashingError(GossipAction.REJECT, {
51
- code: ProposerSlashingErrorCode.INVALID,
52
- error: Error("Invalid signature"),
51
+ code: ProposerSlashingErrorCode.INVALID_SIGNATURE,
53
52
  });
54
53
  }
55
54
  }
@@ -1,8 +1,9 @@
1
1
  import {ChainForkConfig} from "@lodestar/config";
2
2
  import {Db, DbBatch, FilterOptions, KeyValue, Repository} from "@lodestar/db";
3
3
  import {Root, SignedBeaconBlock, Slot, ssz} from "@lodestar/types";
4
- import {bytesToInt, intToBytes} from "@lodestar/utils";
4
+ import {bytesToInt, fromHex, intToBytes} from "@lodestar/utils";
5
5
  import {getSignedBlockTypeFromBytes} from "../../util/multifork.js";
6
+ import {getParentRootFromSignedBeaconBlockSerialized} from "../../util/sszBytes.js";
6
7
  import {Bucket, getBucketNameByValue} from "../buckets.js";
7
8
  import {
8
9
  deleteParentRootIndex,
@@ -24,6 +25,10 @@ export type BlockArchiveBatchPutBinaryItem = KeyValue<Slot, Uint8Array> & {
24
25
  parentRoot: Root;
25
26
  };
26
27
 
28
+ // The initial prune can span millions of slots. This bounds the slot index reads in flight and keeps each
29
+ // delete batch at a few thousand keys, both tiny next to the block value batches in archiveBlocks
30
+ const DELETE_RANGE_CHUNK_SIZE = 1000;
31
+
27
32
  /**
28
33
  * Stores finalized blocks. Block slot is identifier.
29
34
  */
@@ -103,6 +108,38 @@ export class BlockArchiveRepository extends Repository<Slot, SignedBeaconBlock>
103
108
  );
104
109
  }
105
110
 
111
+ /**
112
+ * Delete a contiguous range of archived blocks together with their index entries.
113
+ * `slots` must be every archived slot in the range, so the parent of each block is the entry before it.
114
+ */
115
+ async batchDeleteRange(slots: Slot[]): Promise<void> {
116
+ const sorted = [...slots].sort((a, b) => a - b);
117
+ let prevRoot: Root | null = null;
118
+
119
+ for (let i = 0; i < sorted.length; i += DELETE_RANGE_CHUNK_SIZE) {
120
+ const chunk = sorted.slice(i, i + DELETE_RANGE_CHUNK_SIZE);
121
+ const roots = await Promise.all(chunk.map((slot) => this.getRootBySlot(slot)));
122
+
123
+ const keys: Uint8Array[] = [];
124
+ for (let j = 0; j < chunk.length; j++) {
125
+ keys.push(this.encodeKey(chunk[j]), getSlotIndexKey(chunk[j]));
126
+ const root = roots[j];
127
+ if (root) keys.push(getRootIndexKey(root));
128
+ // The parent index entry pointing at this block is keyed by its parent root, which is the
129
+ // previous block's root unless that block is unindexed, then it is read from the block itself
130
+ let parentRoot = prevRoot;
131
+ if (parentRoot === null) {
132
+ const block = await this.getBinary(chunk[j]);
133
+ const parentRootHex = block ? getParentRootFromSignedBeaconBlockSerialized(block) : null;
134
+ parentRoot = parentRootHex ? fromHex(parentRootHex) : null;
135
+ }
136
+ if (parentRoot) keys.push(getParentRootIndexKey(parentRoot));
137
+ prevRoot = root;
138
+ }
139
+ await this.db.batchDelete(keys, this.dbReqOpts);
140
+ }
141
+ }
142
+
106
143
  async batch(operations: DbBatch<Slot, SignedBeaconBlock>): Promise<void> {
107
144
  await this.batchBinary(
108
145
  operations.map((operation) =>
@@ -150,8 +150,8 @@ export function getGossipSSZType(topic: GossipTopic) {
150
150
  /**
151
151
  * Return the maximum uncompressed SSZ byte length allowed by the type and configured network bounds.
152
152
  */
153
- export function getGossipSSZMaxSize(topic: GossipTopic, config: ChainForkConfig, sszType?: CompositeTypeAny): number {
154
- const maxSize = Math.min((sszType ?? getGossipSSZType(topic)).maxSize, config.MAX_PAYLOAD_SIZE);
153
+ export function getGossipSSZMaxSize(topic: GossipTopic, config: ChainForkConfig, sszType: CompositeTypeAny): number {
154
+ const maxSize = Math.min(sszType.maxSize, config.MAX_PAYLOAD_SIZE);
155
155
  if (isForkPostGloas(topic.boundary.fork) && topic.type === GossipType.data_column_sidecar) {
156
156
  return Math.min(maxSize, computeMaxGloasDataColumnSidecarSize(config));
157
157
  }
@@ -1,24 +1,122 @@
1
1
  import {TopicValidatorResult} from "@libp2p/gossipsub";
2
2
  import {ChainForkConfig} from "@lodestar/config";
3
3
  import {Logger} from "@lodestar/utils";
4
- import {AttestationError, GossipAction, GossipActionError} from "../../chain/errors/index.js";
4
+ import {
5
+ AttestationError,
6
+ AttestationErrorCode,
7
+ AttesterSlashingErrorCode,
8
+ BlobSidecarErrorCode,
9
+ BlockErrorCode,
10
+ BlsToExecutionChangeErrorCode,
11
+ DataColumnSidecarErrorCode,
12
+ ExecutionPayloadBidErrorCode,
13
+ ExecutionPayloadEnvelopeErrorCode,
14
+ GossipAction,
15
+ GossipActionError,
16
+ PayloadAttestationErrorCode,
17
+ ProposerPreferencesErrorCode,
18
+ ProposerSlashingErrorCode,
19
+ SyncCommitteeErrorCode,
20
+ VoluntaryExitErrorCode,
21
+ } from "../../chain/errors/index.js";
5
22
  import {Metrics} from "../../metrics/index.js";
23
+ import {INetworkCore} from "../core/index.js";
6
24
  import {
7
25
  BatchGossipHandlerFn,
8
26
  GossipHandlerFn,
9
27
  GossipHandlers,
10
28
  GossipMessageInfo,
29
+ GossipType,
11
30
  GossipValidatorBatchFn,
12
31
  GossipValidatorFn,
13
32
  } from "../gossip/interface.js";
33
+ import {PeerAction} from "../peers/index.js";
14
34
  import {prettyPrintPeerIdStr} from "../util.js";
15
35
 
16
36
  export type ValidatorFnModules = {
17
37
  config: ChainForkConfig;
18
38
  logger: Logger;
19
39
  metrics: Metrics | null;
40
+ core: INetworkCore;
41
+ };
42
+
43
+ type RejectPeerActionRule = {default: PeerAction; byCode?: Record<string, PeerAction>};
44
+
45
+ /**
46
+ * PeerAction mapping based on the topic and specific codes.
47
+ * Invalid signatures are Fatal on every topic.
48
+ */
49
+ const gossipRejectPeerAction: Record<GossipType, RejectPeerActionRule> = {
50
+ [GossipType.beacon_block]: {
51
+ default: PeerAction.LowToleranceError,
52
+ byCode: {[BlockErrorCode.PROPOSAL_SIGNATURE_INVALID]: PeerAction.Fatal},
53
+ },
54
+ [GossipType.blob_sidecar]: {
55
+ default: PeerAction.LowToleranceError,
56
+ byCode: {[BlobSidecarErrorCode.PROPOSAL_SIGNATURE_INVALID]: PeerAction.Fatal},
57
+ },
58
+ [GossipType.data_column_sidecar]: {
59
+ default: PeerAction.LowToleranceError,
60
+ byCode: {[DataColumnSidecarErrorCode.PROPOSAL_SIGNATURE_INVALID]: PeerAction.Fatal},
61
+ },
62
+ [GossipType.execution_payload]: {
63
+ default: PeerAction.LowToleranceError,
64
+ byCode: {[ExecutionPayloadEnvelopeErrorCode.INVALID_SIGNATURE]: PeerAction.Fatal},
65
+ },
66
+ [GossipType.beacon_aggregate_and_proof]: {
67
+ default: PeerAction.MidToleranceError,
68
+ byCode: {[AttestationErrorCode.INVALID_SIGNATURE]: PeerAction.Fatal},
69
+ },
70
+ [GossipType.beacon_attestation]: {
71
+ default: PeerAction.MidToleranceError,
72
+ byCode: {[AttestationErrorCode.INVALID_SIGNATURE]: PeerAction.Fatal},
73
+ },
74
+ [GossipType.sync_committee_contribution_and_proof]: {
75
+ default: PeerAction.MidToleranceError,
76
+ byCode: {[SyncCommitteeErrorCode.INVALID_SIGNATURE]: PeerAction.Fatal},
77
+ },
78
+ [GossipType.sync_committee]: {
79
+ default: PeerAction.MidToleranceError,
80
+ byCode: {[SyncCommitteeErrorCode.INVALID_SIGNATURE]: PeerAction.Fatal},
81
+ },
82
+ [GossipType.payload_attestation_message]: {
83
+ default: PeerAction.MidToleranceError,
84
+ byCode: {[PayloadAttestationErrorCode.INVALID_SIGNATURE]: PeerAction.Fatal},
85
+ },
86
+ [GossipType.execution_payload_bid]: {
87
+ default: PeerAction.HighToleranceError,
88
+ byCode: {[ExecutionPayloadBidErrorCode.INVALID_SIGNATURE]: PeerAction.Fatal},
89
+ },
90
+ [GossipType.proposer_preferences]: {
91
+ default: PeerAction.HighToleranceError,
92
+ byCode: {[ProposerPreferencesErrorCode.INVALID_SIGNATURE]: PeerAction.Fatal},
93
+ },
94
+ [GossipType.voluntary_exit]: {
95
+ default: PeerAction.HighToleranceError,
96
+ byCode: {[VoluntaryExitErrorCode.INVALID_SIGNATURE]: PeerAction.Fatal},
97
+ },
98
+ [GossipType.bls_to_execution_change]: {
99
+ default: PeerAction.HighToleranceError,
100
+ byCode: {[BlsToExecutionChangeErrorCode.INVALID_SIGNATURE]: PeerAction.Fatal},
101
+ },
102
+ [GossipType.proposer_slashing]: {
103
+ default: PeerAction.HighToleranceError,
104
+ byCode: {[ProposerSlashingErrorCode.INVALID_SIGNATURE]: PeerAction.Fatal},
105
+ },
106
+ [GossipType.attester_slashing]: {
107
+ default: PeerAction.HighToleranceError,
108
+ byCode: {[AttesterSlashingErrorCode.INVALID_SIGNATURE]: PeerAction.Fatal},
109
+ },
110
+ // light client validators only throw IGNORE
111
+ [GossipType.light_client_finality_update]: {default: PeerAction.HighToleranceError},
112
+ [GossipType.light_client_optimistic_update]: {default: PeerAction.HighToleranceError},
20
113
  };
21
114
 
115
+ function rejectPeerAction(type: GossipType, code: string): PeerAction {
116
+ const rule = gossipRejectPeerAction[type];
117
+ return rule.byCode?.[code] ?? rule.default;
118
+ }
119
+
22
120
  /**
23
121
  * Similar to getGossipValidatorFn but return a function to accept a batch of beacon_attestation messages
24
122
  * with the same attestation data
@@ -27,7 +125,7 @@ export function getGossipValidatorBatchFn(
27
125
  gossipHandlers: GossipHandlers,
28
126
  modules: ValidatorFnModules
29
127
  ): GossipValidatorBatchFn {
30
- const {logger, metrics} = modules;
128
+ const {logger, metrics, core} = modules;
31
129
 
32
130
  return async function gossipValidatorBatchFn(messageInfos: GossipMessageInfo[]) {
33
131
  // all messageInfos have same topic type
@@ -69,16 +167,19 @@ export function getGossipValidatorBatchFn(
69
167
  // only beacon_attestation topic is validated in batch
70
168
  metrics?.networkProcessor.gossipAttestationIgnoreByReason.inc({reason: e.type.code});
71
169
  return TopicValidatorResult.Ignore;
72
- case GossipAction.REJECT:
170
+ case GossipAction.REJECT: {
73
171
  metrics?.networkProcessor.gossipValidationReject.inc({topic: type});
74
172
  // only beacon_attestation topic is validated in batch
75
173
  metrics?.networkProcessor.gossipAttestationRejectByReason.inc({reason: e.type.code});
174
+ const peerAction = rejectPeerAction(type, e.type.code);
175
+ core.reportPeer(propagationSource, peerAction, e.type.code);
76
176
  logger.debug(
77
177
  `Gossip validation ${type} rejected`,
78
- {peerId: prettyPrintPeerIdStr(propagationSource), clientAgent, clientVersion},
178
+ {peer: propagationSource, clientAgent, clientVersion, peerAction},
79
179
  e
80
180
  );
81
181
  return TopicValidatorResult.Reject;
182
+ }
82
183
  }
83
184
  });
84
185
  } catch (e) {
@@ -108,7 +209,7 @@ export function getGossipValidatorBatchFn(
108
209
  * @see getGossipHandlers for reasoning on why GossipHandlerFn are used for gossip validation.
109
210
  */
110
211
  export function getGossipValidatorFn(gossipHandlers: GossipHandlers, modules: ValidatorFnModules): GossipValidatorFn {
111
- const {logger, metrics} = modules;
212
+ const {logger, metrics, core} = modules;
112
213
 
113
214
  return async function gossipValidatorFn({
114
215
  topic,
@@ -155,14 +256,17 @@ export function getGossipValidatorFn(gossipHandlers: GossipHandlers, modules: Va
155
256
  metrics?.networkProcessor.gossipValidationIgnore.inc({topic: type});
156
257
  return TopicValidatorResult.Ignore;
157
258
 
158
- case GossipAction.REJECT:
259
+ case GossipAction.REJECT: {
159
260
  metrics?.networkProcessor.gossipValidationReject.inc({topic: type});
261
+ const peerAction = rejectPeerAction(type, e.type.code);
262
+ core.reportPeer(propagationSource, peerAction, e.type.code);
160
263
  logger.debug(
161
264
  `Gossip validation ${type} rejected`,
162
- {peerId: prettyPrintPeerIdStr(propagationSource), clientAgent, clientVersion},
265
+ {peer: propagationSource, clientAgent, clientVersion, peerAction},
163
266
  e
164
267
  );
165
268
  return TopicValidatorResult.Reject;
269
+ }
166
270
  }
167
271
  }
168
272
  };
@@ -86,8 +86,13 @@ export const MAX_AWAITING_MESSAGES_PER_ROOT: Partial<Record<GossipType, number>>
86
86
 
87
87
  /**
88
88
  * Track the forwarded peers we already emit to UnknownBlockInput sync.
89
+ * envelopeSlotEmitted: whether the unknownEnvelopeBlockRootSlot event was already emitted for this root.
89
90
  */
90
- type SearchedRootEntry = {blockPeerIds?: Set<PeerIdStr>; envelopePeerIds?: Set<PeerIdStr>};
91
+ type SearchedRootEntry = {
92
+ blockPeerIds?: Set<PeerIdStr>;
93
+ envelopePeerIds?: Set<PeerIdStr>;
94
+ envelopeSlotEmitted?: boolean;
95
+ };
91
96
 
92
97
  /**
93
98
  * This is respective to gossipsub seenTTL (which is 550 * 0.7 = 385s), also it's respective
@@ -347,12 +352,17 @@ export class NetworkProcessor {
347
352
  }
348
353
 
349
354
  /**
350
- * Search envelope via `ChainEvent.unknownEnvelopeBlockRoot` event
355
+ * Search envelope via `ChainEvent.unknownEnvelopeBlockRoot` / `ChainEvent.unknownEnvelopeBlockRootSlot` event
351
356
  * Slot is the message slot, which is not necessarily the same as the envelope's slot, but it can be used for a good prune strategy.
352
357
  * In the rare case, if 2 messages on 2 slots search for the same root (for example beacon_attestation) we may emit the same root twice but BlockInputSync should handle it well.
353
358
  * Same peer-forwarding + dedup behavior as searchUnknownBlock.
354
359
  */
355
- searchUnknownEnvelope({slot, root}: SlotRootHex, source: BlockInputSource, peer?: PeerIdStr): void {
360
+ searchUnknownEnvelope(
361
+ {slot, root}: SlotRootHex,
362
+ source: BlockInputSource,
363
+ peer?: PeerIdStr,
364
+ slotIsPayloadSlot = false
365
+ ): void {
356
366
  if (this.chain.seenPayloadEnvelope(root)) {
357
367
  return;
358
368
  }
@@ -369,8 +379,17 @@ export class NetworkProcessor {
369
379
  forwardedPeers.add(peer);
370
380
  shouldEmit = true;
371
381
  }
382
+ // the payload-slot seed must reach the sync once, even if a slot-less trigger started the search first
383
+ if (slotIsPayloadSlot && !entry.envelopeSlotEmitted) {
384
+ shouldEmit = true;
385
+ }
372
386
  if (shouldEmit) {
373
- this.chain.emitter.emit(ChainEvent.unknownEnvelopeBlockRoot, {rootHex: root, slot, peer, source});
387
+ if (slotIsPayloadSlot) {
388
+ entry.envelopeSlotEmitted = true;
389
+ this.chain.emitter.emit(ChainEvent.unknownEnvelopeBlockRootSlot, {rootHex: root, slot, peer, source});
390
+ } else {
391
+ this.chain.emitter.emit(ChainEvent.unknownEnvelopeBlockRoot, {rootHex: root, peer, source});
392
+ }
374
393
  }
375
394
  }
376
395
 
@@ -494,8 +513,9 @@ export class NetworkProcessor {
494
513
  if (root == null) break;
495
514
  const payloadPresent = getPayloadPresentFromPayloadAttestationMessageSerialized(message.msg.data);
496
515
  if (payloadPresent && !this.chain.forkChoice.hasPayloadHexUnsafe(root)) {
497
- // payload attestation votes that the payload is available but it is not yet known
498
- this.searchUnknownRoot({slot, root}, false, true, peerId);
516
+ // payload attestation votes that the payload is available but it is not yet known.
517
+ // the PTC vote's slot is the payload's slot
518
+ this.searchUnknownRoot({slot, root}, false, true, peerId, true);
499
519
  // do not await the envelope, payload attestation processing only requires that the block is known
500
520
  // also do not reset preprocessResult, we may already await for the block
501
521
  }
@@ -504,7 +524,8 @@ export class NetworkProcessor {
504
524
  case GossipType.data_column_sidecar: {
505
525
  if (root == null) break;
506
526
  if (!this.chain.forkChoice.hasPayloadHexUnsafe(root)) {
507
- this.searchUnknownRoot({slot, root}, false, true, peerId);
527
+ // the sidecar's slot is the block's (and so the payload's) slot
528
+ this.searchUnknownRoot({slot, root}, false, true, peerId, true);
508
529
  // do not await the envelope, we can do gossip validation
509
530
  // also do not reset preprocessResult, we may already await for the block
510
531
  }
@@ -641,16 +662,23 @@ export class NetworkProcessor {
641
662
  return true;
642
663
  }
643
664
 
665
+ /**
666
+ * Search block/envelope given a SlotRootHex
667
+ * envelopeSlotIsPayloadSlot: the message slot is the payload's slot or not
668
+ */
644
669
  private searchUnknownRoot(
645
670
  slotRoot: SlotRootHex,
646
671
  searchBlock: boolean,
647
672
  searchEnvelope: boolean,
648
- peerId: PeerIdStr
673
+ peerId: PeerIdStr,
674
+ envelopeSlotIsPayloadSlot = false
649
675
  ): void {
650
676
  if (!searchBlock && !searchEnvelope) return;
651
677
  if (this.tooManySearchedRoots(slotRoot.slot, slotRoot.root)) return;
652
678
  if (searchBlock) this.searchUnknownBlock(slotRoot, BlockInputSource.network_processor, peerId);
653
- if (searchEnvelope) this.searchUnknownEnvelope(slotRoot, BlockInputSource.network_processor, peerId);
679
+ if (searchEnvelope) {
680
+ this.searchUnknownEnvelope(slotRoot, BlockInputSource.network_processor, peerId, envelopeSlotIsPayloadSlot);
681
+ }
654
682
  }
655
683
 
656
684
  private pushPendingGossipsubMessageToQueue(message: PendingGossipsubMessage): void {
package/src/sync/types.ts CHANGED
@@ -23,6 +23,10 @@ export enum PendingBlockType {
23
23
  * Payload analog of UNKNOWN_BLOCK_ROOT: we have a beacon block root but not its execution payload envelope.
24
24
  */
25
25
  UNKNOWN_PAYLOAD_BLOCK_ROOT = "unknown_payload_block_root",
26
+ /**
27
+ * Same as UNKNOWN_PAYLOAD_BLOCK_ROOT, but the trigger also carries the payload's slot.
28
+ */
29
+ UNKNOWN_PAYLOAD_BLOCK_ROOT_SLOT = "unknown_payload_block_root_slot",
26
30
  /**
27
31
  * Payload analog of INCOMPLETE_BLOCK_INPUT: we have a partial payload input that did not complete in time.
28
32
  */
@@ -130,9 +134,8 @@ export type PendingPayloadInput = {
130
134
  export type PendingPayloadRootHex = {
131
135
  status: PendingPayloadInputStatus.pending | PendingPayloadInputStatus.fetching;
132
136
  rootHex: RootHex;
133
- // Trusted slot only (fork choice / validated data), may be missing until resolved. NOT the gossip
134
- // message slot from ChainEvent.unknownEnvelopeBlockRoot, which is untrusted and not necessarily the
135
- // payload/block slot. See BlockInputSync.resolvePayloadSlot.
137
+ // slot could be via fork choice or ChainEvent.unknownEnvelopeBlockRootSlot
138
+ // while ChainEvent.unknownEnvelopeBlockRoot does not provide one
136
139
  slot?: Slot;
137
140
  timeAddedSec: number;
138
141
  timeSyncedSec?: number;
@@ -152,6 +152,7 @@ export class BlockInputSync {
152
152
  this.logger.verbose("BlockInputSync enabled.");
153
153
  this.chain.emitter.on(ChainEvent.unknownBlockRoot, this.onUnknownBlockRoot);
154
154
  this.chain.emitter.on(ChainEvent.unknownEnvelopeBlockRoot, this.onUnknownEnvelopeBlockRoot);
155
+ this.chain.emitter.on(ChainEvent.unknownEnvelopeBlockRootSlot, this.onUnknownEnvelopeBlockRootSlot);
155
156
  this.chain.emitter.on(ChainEvent.incompleteBlockInput, this.onIncompleteBlockInput);
156
157
  this.chain.emitter.on(ChainEvent.incompletePayloadEnvelope, this.onIncompletePayloadEnvelope);
157
158
  this.chain.emitter.on(ChainEvent.blockUnknownParent, this.onUnknownParent);
@@ -179,6 +180,7 @@ export class BlockInputSync {
179
180
  this.clearRateLimitBackoffTimer();
180
181
  this.chain.emitter.off(ChainEvent.unknownBlockRoot, this.onUnknownBlockRoot);
181
182
  this.chain.emitter.off(ChainEvent.unknownEnvelopeBlockRoot, this.onUnknownEnvelopeBlockRoot);
183
+ this.chain.emitter.off(ChainEvent.unknownEnvelopeBlockRootSlot, this.onUnknownEnvelopeBlockRootSlot);
182
184
  this.chain.emitter.off(ChainEvent.incompleteBlockInput, this.onIncompleteBlockInput);
183
185
  this.chain.emitter.off(ChainEvent.incompletePayloadEnvelope, this.onIncompletePayloadEnvelope);
184
186
  this.chain.emitter.off(ChainEvent.blockUnknownParent, this.onUnknownParent);
@@ -242,6 +244,19 @@ export class BlockInputSync {
242
244
  }
243
245
  };
244
246
 
247
+ private onUnknownEnvelopeBlockRootSlot = (data: ChainEventData[ChainEvent.unknownEnvelopeBlockRootSlot]): void => {
248
+ try {
249
+ const isNewRoot = this.addByPayloadRootHex(data.rootHex, data.peer, data.slot);
250
+ if (isNewRoot) {
251
+ this.triggerUnknownBlockSearch();
252
+ this.metrics?.blockInputSync.requests.inc({type: PendingBlockType.UNKNOWN_PAYLOAD_BLOCK_ROOT_SLOT});
253
+ this.metrics?.blockInputSync.payloadSource.inc({source: data.source});
254
+ }
255
+ } catch (e) {
256
+ this.logger.debug("Error handling unknownEnvelopeBlockRootSlot event", {}, e as Error);
257
+ }
258
+ };
259
+
245
260
  private onIncompletePayloadEnvelope = (data: ChainEventData[ChainEvent.incompletePayloadEnvelope]): void => {
246
261
  try {
247
262
  this.addByPayloadInput(data.payloadInput, data.peer);