From 8c66b3c0266b84d394a1f957dc0cfff1f0551501 Mon Sep 17 00:00:00 2001 From: twoeths Date: Fri, 11 Sep 2026 15:15:44 +0700 Subject: [PATCH 1/5] feat: penalize peers for forwarding REJECTED gossip messages --- .../network/processor/gossipValidatorFn.ts | 42 ++++- .../processor/gossipValidatorFn.test.ts | 157 ++++++++++++++++++ 2 files changed, 191 insertions(+), 8 deletions(-) create mode 100644 packages/beacon-node/test/unit/network/processor/gossipValidatorFn.test.ts diff --git a/packages/beacon-node/src/network/processor/gossipValidatorFn.ts b/packages/beacon-node/src/network/processor/gossipValidatorFn.ts index 19442c7173c4..5d22975c7926 100644 --- a/packages/beacon-node/src/network/processor/gossipValidatorFn.ts +++ b/packages/beacon-node/src/network/processor/gossipValidatorFn.ts @@ -3,20 +3,48 @@ import {ChainForkConfig} from "@lodestar/config"; import {Logger} from "@lodestar/utils"; import {AttestationError, GossipAction, GossipActionError} from "../../chain/errors/index.js"; import {Metrics} from "../../metrics/index.js"; +import {INetworkCore} from "../core/index.js"; import { BatchGossipHandlerFn, GossipHandlerFn, GossipHandlers, GossipMessageInfo, + GossipType, GossipValidatorBatchFn, GossipValidatorFn, } from "../gossip/interface.js"; +import {PeerAction} from "../peers/index.js"; import {prettyPrintPeerIdStr} from "../util.js"; export type ValidatorFnModules = { config: ChainForkConfig; logger: Logger; metrics: Metrics | null; + core: INetworkCore; +}; + +/** + * Critical topics (the `bypassQueue` set in + * `processor/index.ts`) are penalized heavily; the rest with mid tolerance. + */ +const gossipRejectPeerAction: Record = { + [GossipType.beacon_block]: PeerAction.LowToleranceError, + [GossipType.blob_sidecar]: PeerAction.LowToleranceError, + [GossipType.data_column_sidecar]: PeerAction.LowToleranceError, + [GossipType.execution_payload]: PeerAction.LowToleranceError, + [GossipType.beacon_aggregate_and_proof]: PeerAction.MidToleranceError, + [GossipType.beacon_attestation]: PeerAction.MidToleranceError, + [GossipType.voluntary_exit]: PeerAction.MidToleranceError, + [GossipType.proposer_slashing]: PeerAction.MidToleranceError, + [GossipType.attester_slashing]: PeerAction.MidToleranceError, + [GossipType.sync_committee_contribution_and_proof]: PeerAction.MidToleranceError, + [GossipType.sync_committee]: PeerAction.MidToleranceError, + [GossipType.light_client_finality_update]: PeerAction.MidToleranceError, + [GossipType.light_client_optimistic_update]: PeerAction.MidToleranceError, + [GossipType.bls_to_execution_change]: PeerAction.MidToleranceError, + [GossipType.payload_attestation_message]: PeerAction.MidToleranceError, + [GossipType.execution_payload_bid]: PeerAction.MidToleranceError, + [GossipType.proposer_preferences]: PeerAction.MidToleranceError, }; /** @@ -27,7 +55,7 @@ export function getGossipValidatorBatchFn( gossipHandlers: GossipHandlers, modules: ValidatorFnModules ): GossipValidatorBatchFn { - const {logger, metrics} = modules; + const {logger, metrics, core} = modules; return async function gossipValidatorBatchFn(messageInfos: GossipMessageInfo[]) { // all messageInfos have same topic type @@ -73,9 +101,10 @@ export function getGossipValidatorBatchFn( metrics?.networkProcessor.gossipValidationReject.inc({topic: type}); // only beacon_attestation topic is validated in batch metrics?.networkProcessor.gossipAttestationRejectByReason.inc({reason: e.type.code}); + core.reportPeer(propagationSource, gossipRejectPeerAction[type], e.type.code); logger.debug( `Gossip validation ${type} rejected`, - {peerId: prettyPrintPeerIdStr(propagationSource), clientAgent, clientVersion}, + {peer: propagationSource, clientAgent, clientVersion}, e ); return TopicValidatorResult.Reject; @@ -108,7 +137,7 @@ export function getGossipValidatorBatchFn( * @see getGossipHandlers for reasoning on why GossipHandlerFn are used for gossip validation. */ export function getGossipValidatorFn(gossipHandlers: GossipHandlers, modules: ValidatorFnModules): GossipValidatorFn { - const {logger, metrics} = modules; + const {logger, metrics, core} = modules; return async function gossipValidatorFn({ topic, @@ -157,11 +186,8 @@ export function getGossipValidatorFn(gossipHandlers: GossipHandlers, modules: Va case GossipAction.REJECT: metrics?.networkProcessor.gossipValidationReject.inc({topic: type}); - logger.debug( - `Gossip validation ${type} rejected`, - {peerId: prettyPrintPeerIdStr(propagationSource), clientAgent, clientVersion}, - e - ); + core.reportPeer(propagationSource, gossipRejectPeerAction[type], e.type.code); + logger.debug(`Gossip validation ${type} rejected`, {peer: propagationSource, clientAgent, clientVersion}, e); return TopicValidatorResult.Reject; } } diff --git a/packages/beacon-node/test/unit/network/processor/gossipValidatorFn.test.ts b/packages/beacon-node/test/unit/network/processor/gossipValidatorFn.test.ts new file mode 100644 index 000000000000..c0e9ad58a3f3 --- /dev/null +++ b/packages/beacon-node/test/unit/network/processor/gossipValidatorFn.test.ts @@ -0,0 +1,157 @@ +import {TopicValidatorResult} from "@libp2p/gossipsub"; +import {beforeEach, describe, expect, it, vi} from "vitest"; +import {config} from "@lodestar/config/default"; +import {testLogger} from "@lodestar/logger/test-utils"; +import {AttestationError, AttestationErrorCode} from "../../../../src/chain/errors/attestationError.js"; +import {BlockErrorCode, BlockGossipError} from "../../../../src/chain/errors/blockError.js"; +import {GossipAction} from "../../../../src/chain/errors/gossipValidation.js"; +import {INetworkCore} from "../../../../src/network/core/index.js"; +import {GossipHandlers, GossipMessageInfo, GossipTopic, GossipType} from "../../../../src/network/gossip/interface.js"; +import {PeerAction} from "../../../../src/network/peers/index.js"; +import { + ValidatorFnModules, + getGossipValidatorBatchFn, + getGossipValidatorFn, +} from "../../../../src/network/processor/gossipValidatorFn.js"; + +describe("gossipValidatorFn", () => { + const logger = testLogger(); + const peerIdStr = "16Uiu2HAmFakePeerIdForGossipValidatorFnTest"; + + let core: INetworkCore; + let modules: ValidatorFnModules; + + beforeEach(() => { + core = {reportPeer: vi.fn()} as unknown as INetworkCore; + modules = {config, logger, metrics: null, core}; + }); + + function getMessageInfo(type: GossipType): GossipMessageInfo { + return { + topic: {type} as GossipTopic, + msg: {data: new Uint8Array()} as GossipMessageInfo["msg"], + propagationSource: peerIdStr, + clientAgent: "Unknown", + clientVersion: "", + seenTimestampSec: Date.now() / 1000, + msgSlot: null, + }; + } + + function getHandlers(type: GossipType, handler: () => Promise): GossipHandlers { + return {[type]: handler} as unknown as GossipHandlers; + } + + const rejectError = new BlockGossipError(GossipAction.REJECT, { + code: BlockErrorCode.PROPOSAL_SIGNATURE_INVALID, + slot: 100, + root: "0x1234", + }); + + const ignoreError = new BlockGossipError(GossipAction.IGNORE, { + code: BlockErrorCode.ALREADY_KNOWN, + root: "0x1234", + }); + + it("reports peer with LowToleranceError on beacon_block REJECT", async () => { + const validatorFn = getGossipValidatorFn( + getHandlers(GossipType.beacon_block, () => Promise.reject(rejectError)), + modules + ); + + const result = await validatorFn(getMessageInfo(GossipType.beacon_block)); + + expect(result).toBe(TopicValidatorResult.Reject); + expect(core.reportPeer).toHaveBeenCalledOnce(); + expect(core.reportPeer).toHaveBeenCalledWith( + peerIdStr, + PeerAction.LowToleranceError, + BlockErrorCode.PROPOSAL_SIGNATURE_INVALID + ); + }); + + it("reports peer with MidToleranceError on non-critical topic REJECT", async () => { + const validatorFn = getGossipValidatorFn( + getHandlers(GossipType.beacon_aggregate_and_proof, () => Promise.reject(rejectError)), + modules + ); + + const result = await validatorFn(getMessageInfo(GossipType.beacon_aggregate_and_proof)); + + expect(result).toBe(TopicValidatorResult.Reject); + expect(core.reportPeer).toHaveBeenCalledOnce(); + expect(core.reportPeer).toHaveBeenCalledWith( + peerIdStr, + PeerAction.MidToleranceError, + BlockErrorCode.PROPOSAL_SIGNATURE_INVALID + ); + }); + + it("does not report peer on IGNORE", async () => { + const validatorFn = getGossipValidatorFn( + getHandlers(GossipType.beacon_block, () => Promise.reject(ignoreError)), + modules + ); + + const result = await validatorFn(getMessageInfo(GossipType.beacon_block)); + + expect(result).toBe(TopicValidatorResult.Ignore); + expect(core.reportPeer).not.toHaveBeenCalled(); + }); + + it("does not report peer on non-GossipActionError", async () => { + const validatorFn = getGossipValidatorFn( + getHandlers(GossipType.beacon_block, () => Promise.reject(new Error("unexpected"))), + modules + ); + + const result = await validatorFn(getMessageInfo(GossipType.beacon_block)); + + expect(result).toBe(TopicValidatorResult.Ignore); + expect(core.reportPeer).not.toHaveBeenCalled(); + }); + + it("does not report peer on ACCEPT", async () => { + const validatorFn = getGossipValidatorFn( + getHandlers(GossipType.beacon_block, () => Promise.resolve()), + modules + ); + + const result = await validatorFn(getMessageInfo(GossipType.beacon_block)); + + expect(result).toBe(TopicValidatorResult.Accept); + expect(core.reportPeer).not.toHaveBeenCalled(); + }); + + describe("batch beacon_attestation", () => { + const attRejectError = new AttestationError(GossipAction.REJECT, { + code: AttestationErrorCode.INVALID_SIGNATURE, + }); + const attIgnoreError = new AttestationError(GossipAction.IGNORE, { + code: AttestationErrorCode.BAD_TARGET_EPOCH, + }); + + it("reports only peers of rejected messages", async () => { + const gossipHandlers = { + [GossipType.beacon_attestation]: () => Promise.resolve([null, attRejectError, attIgnoreError]), + } as unknown as GossipHandlers; + const validatorBatchFn = getGossipValidatorBatchFn(gossipHandlers, modules); + + const messageInfos = [ + getMessageInfo(GossipType.beacon_attestation), + {...getMessageInfo(GossipType.beacon_attestation), propagationSource: "rejected-peer"}, + getMessageInfo(GossipType.beacon_attestation), + ]; + + const results = await validatorBatchFn(messageInfos); + + expect(results).toEqual([TopicValidatorResult.Accept, TopicValidatorResult.Reject, TopicValidatorResult.Ignore]); + expect(core.reportPeer).toHaveBeenCalledOnce(); + expect(core.reportPeer).toHaveBeenCalledWith( + "rejected-peer", + PeerAction.MidToleranceError, + AttestationErrorCode.INVALID_SIGNATURE + ); + }); + }); +}); From 004b5cfe216755cc2340de08d0629206f0a74488 Mon Sep 17 00:00:00 2001 From: twoeths Date: Fri, 11 Sep 2026 15:20:14 +0700 Subject: [PATCH 2/5] chore: remove unit test --- .../processor/gossipValidatorFn.test.ts | 157 ------------------ 1 file changed, 157 deletions(-) delete mode 100644 packages/beacon-node/test/unit/network/processor/gossipValidatorFn.test.ts diff --git a/packages/beacon-node/test/unit/network/processor/gossipValidatorFn.test.ts b/packages/beacon-node/test/unit/network/processor/gossipValidatorFn.test.ts deleted file mode 100644 index c0e9ad58a3f3..000000000000 --- a/packages/beacon-node/test/unit/network/processor/gossipValidatorFn.test.ts +++ /dev/null @@ -1,157 +0,0 @@ -import {TopicValidatorResult} from "@libp2p/gossipsub"; -import {beforeEach, describe, expect, it, vi} from "vitest"; -import {config} from "@lodestar/config/default"; -import {testLogger} from "@lodestar/logger/test-utils"; -import {AttestationError, AttestationErrorCode} from "../../../../src/chain/errors/attestationError.js"; -import {BlockErrorCode, BlockGossipError} from "../../../../src/chain/errors/blockError.js"; -import {GossipAction} from "../../../../src/chain/errors/gossipValidation.js"; -import {INetworkCore} from "../../../../src/network/core/index.js"; -import {GossipHandlers, GossipMessageInfo, GossipTopic, GossipType} from "../../../../src/network/gossip/interface.js"; -import {PeerAction} from "../../../../src/network/peers/index.js"; -import { - ValidatorFnModules, - getGossipValidatorBatchFn, - getGossipValidatorFn, -} from "../../../../src/network/processor/gossipValidatorFn.js"; - -describe("gossipValidatorFn", () => { - const logger = testLogger(); - const peerIdStr = "16Uiu2HAmFakePeerIdForGossipValidatorFnTest"; - - let core: INetworkCore; - let modules: ValidatorFnModules; - - beforeEach(() => { - core = {reportPeer: vi.fn()} as unknown as INetworkCore; - modules = {config, logger, metrics: null, core}; - }); - - function getMessageInfo(type: GossipType): GossipMessageInfo { - return { - topic: {type} as GossipTopic, - msg: {data: new Uint8Array()} as GossipMessageInfo["msg"], - propagationSource: peerIdStr, - clientAgent: "Unknown", - clientVersion: "", - seenTimestampSec: Date.now() / 1000, - msgSlot: null, - }; - } - - function getHandlers(type: GossipType, handler: () => Promise): GossipHandlers { - return {[type]: handler} as unknown as GossipHandlers; - } - - const rejectError = new BlockGossipError(GossipAction.REJECT, { - code: BlockErrorCode.PROPOSAL_SIGNATURE_INVALID, - slot: 100, - root: "0x1234", - }); - - const ignoreError = new BlockGossipError(GossipAction.IGNORE, { - code: BlockErrorCode.ALREADY_KNOWN, - root: "0x1234", - }); - - it("reports peer with LowToleranceError on beacon_block REJECT", async () => { - const validatorFn = getGossipValidatorFn( - getHandlers(GossipType.beacon_block, () => Promise.reject(rejectError)), - modules - ); - - const result = await validatorFn(getMessageInfo(GossipType.beacon_block)); - - expect(result).toBe(TopicValidatorResult.Reject); - expect(core.reportPeer).toHaveBeenCalledOnce(); - expect(core.reportPeer).toHaveBeenCalledWith( - peerIdStr, - PeerAction.LowToleranceError, - BlockErrorCode.PROPOSAL_SIGNATURE_INVALID - ); - }); - - it("reports peer with MidToleranceError on non-critical topic REJECT", async () => { - const validatorFn = getGossipValidatorFn( - getHandlers(GossipType.beacon_aggregate_and_proof, () => Promise.reject(rejectError)), - modules - ); - - const result = await validatorFn(getMessageInfo(GossipType.beacon_aggregate_and_proof)); - - expect(result).toBe(TopicValidatorResult.Reject); - expect(core.reportPeer).toHaveBeenCalledOnce(); - expect(core.reportPeer).toHaveBeenCalledWith( - peerIdStr, - PeerAction.MidToleranceError, - BlockErrorCode.PROPOSAL_SIGNATURE_INVALID - ); - }); - - it("does not report peer on IGNORE", async () => { - const validatorFn = getGossipValidatorFn( - getHandlers(GossipType.beacon_block, () => Promise.reject(ignoreError)), - modules - ); - - const result = await validatorFn(getMessageInfo(GossipType.beacon_block)); - - expect(result).toBe(TopicValidatorResult.Ignore); - expect(core.reportPeer).not.toHaveBeenCalled(); - }); - - it("does not report peer on non-GossipActionError", async () => { - const validatorFn = getGossipValidatorFn( - getHandlers(GossipType.beacon_block, () => Promise.reject(new Error("unexpected"))), - modules - ); - - const result = await validatorFn(getMessageInfo(GossipType.beacon_block)); - - expect(result).toBe(TopicValidatorResult.Ignore); - expect(core.reportPeer).not.toHaveBeenCalled(); - }); - - it("does not report peer on ACCEPT", async () => { - const validatorFn = getGossipValidatorFn( - getHandlers(GossipType.beacon_block, () => Promise.resolve()), - modules - ); - - const result = await validatorFn(getMessageInfo(GossipType.beacon_block)); - - expect(result).toBe(TopicValidatorResult.Accept); - expect(core.reportPeer).not.toHaveBeenCalled(); - }); - - describe("batch beacon_attestation", () => { - const attRejectError = new AttestationError(GossipAction.REJECT, { - code: AttestationErrorCode.INVALID_SIGNATURE, - }); - const attIgnoreError = new AttestationError(GossipAction.IGNORE, { - code: AttestationErrorCode.BAD_TARGET_EPOCH, - }); - - it("reports only peers of rejected messages", async () => { - const gossipHandlers = { - [GossipType.beacon_attestation]: () => Promise.resolve([null, attRejectError, attIgnoreError]), - } as unknown as GossipHandlers; - const validatorBatchFn = getGossipValidatorBatchFn(gossipHandlers, modules); - - const messageInfos = [ - getMessageInfo(GossipType.beacon_attestation), - {...getMessageInfo(GossipType.beacon_attestation), propagationSource: "rejected-peer"}, - getMessageInfo(GossipType.beacon_attestation), - ]; - - const results = await validatorBatchFn(messageInfos); - - expect(results).toEqual([TopicValidatorResult.Accept, TopicValidatorResult.Reject, TopicValidatorResult.Ignore]); - expect(core.reportPeer).toHaveBeenCalledOnce(); - expect(core.reportPeer).toHaveBeenCalledWith( - "rejected-peer", - PeerAction.MidToleranceError, - AttestationErrorCode.INVALID_SIGNATURE - ); - }); - }); -}); From 615dd0bdb1120b778987921bc08c8626cb5cf6ee Mon Sep 17 00:00:00 2001 From: twoeths Date: Tue, 15 Sep 2026 10:28:21 +0700 Subject: [PATCH 3/5] fix: derive peer action from topic + error code --- .../network/processor/gossipValidatorFn.ts | 88 +++++++++++++------ 1 file changed, 61 insertions(+), 27 deletions(-) diff --git a/packages/beacon-node/src/network/processor/gossipValidatorFn.ts b/packages/beacon-node/src/network/processor/gossipValidatorFn.ts index 5d22975c7926..8b7bcba82304 100644 --- a/packages/beacon-node/src/network/processor/gossipValidatorFn.ts +++ b/packages/beacon-node/src/network/processor/gossipValidatorFn.ts @@ -1,7 +1,15 @@ import {TopicValidatorResult} from "@libp2p/gossipsub"; import {ChainForkConfig} from "@lodestar/config"; import {Logger} from "@lodestar/utils"; -import {AttestationError, GossipAction, GossipActionError} from "../../chain/errors/index.js"; +import { + AttestationError, + BlsToExecutionChangeErrorCode, + ExecutionPayloadBidErrorCode, + GossipAction, + GossipActionError, + ProposerPreferencesErrorCode, + VoluntaryExitErrorCode, +} from "../../chain/errors/index.js"; import {Metrics} from "../../metrics/index.js"; import {INetworkCore} from "../core/index.js"; import { @@ -23,30 +31,48 @@ export type ValidatorFnModules = { core: INetworkCore; }; +type RejectPeerActionRule = {default: PeerAction; byCode?: Record}; + /** - * Critical topics (the `bypassQueue` set in - * `processor/index.ts`) are penalized heavily; the rest with mid tolerance. + * PeerAction mapping based on the topic and specific codes. */ -const gossipRejectPeerAction: Record = { - [GossipType.beacon_block]: PeerAction.LowToleranceError, - [GossipType.blob_sidecar]: PeerAction.LowToleranceError, - [GossipType.data_column_sidecar]: PeerAction.LowToleranceError, - [GossipType.execution_payload]: PeerAction.LowToleranceError, - [GossipType.beacon_aggregate_and_proof]: PeerAction.MidToleranceError, - [GossipType.beacon_attestation]: PeerAction.MidToleranceError, - [GossipType.voluntary_exit]: PeerAction.MidToleranceError, - [GossipType.proposer_slashing]: PeerAction.MidToleranceError, - [GossipType.attester_slashing]: PeerAction.MidToleranceError, - [GossipType.sync_committee_contribution_and_proof]: PeerAction.MidToleranceError, - [GossipType.sync_committee]: PeerAction.MidToleranceError, - [GossipType.light_client_finality_update]: PeerAction.MidToleranceError, - [GossipType.light_client_optimistic_update]: PeerAction.MidToleranceError, - [GossipType.bls_to_execution_change]: PeerAction.MidToleranceError, - [GossipType.payload_attestation_message]: PeerAction.MidToleranceError, - [GossipType.execution_payload_bid]: PeerAction.MidToleranceError, - [GossipType.proposer_preferences]: PeerAction.MidToleranceError, +const gossipRejectPeerAction: Record = { + [GossipType.beacon_block]: {default: PeerAction.LowToleranceError}, + [GossipType.blob_sidecar]: {default: PeerAction.LowToleranceError}, + [GossipType.data_column_sidecar]: {default: PeerAction.LowToleranceError}, + [GossipType.execution_payload]: {default: PeerAction.LowToleranceError}, + [GossipType.beacon_aggregate_and_proof]: {default: PeerAction.MidToleranceError}, + [GossipType.beacon_attestation]: {default: PeerAction.MidToleranceError}, + [GossipType.sync_committee_contribution_and_proof]: {default: PeerAction.MidToleranceError}, + [GossipType.sync_committee]: {default: PeerAction.MidToleranceError}, + [GossipType.payload_attestation_message]: {default: PeerAction.MidToleranceError}, + [GossipType.execution_payload_bid]: { + default: PeerAction.HighToleranceError, + byCode: {[ExecutionPayloadBidErrorCode.INVALID_SIGNATURE]: PeerAction.MidToleranceError}, + }, + [GossipType.proposer_preferences]: { + default: PeerAction.HighToleranceError, + byCode: {[ProposerPreferencesErrorCode.INVALID_SIGNATURE]: PeerAction.MidToleranceError}, + }, + [GossipType.voluntary_exit]: { + default: PeerAction.HighToleranceError, + byCode: {[VoluntaryExitErrorCode.INVALID_SIGNATURE]: PeerAction.MidToleranceError}, + }, + [GossipType.bls_to_execution_change]: { + default: PeerAction.HighToleranceError, + byCode: {[BlsToExecutionChangeErrorCode.INVALID_SIGNATURE]: PeerAction.MidToleranceError}, + }, + [GossipType.proposer_slashing]: {default: PeerAction.HighToleranceError}, + [GossipType.attester_slashing]: {default: PeerAction.HighToleranceError}, + [GossipType.light_client_finality_update]: {default: PeerAction.HighToleranceError}, + [GossipType.light_client_optimistic_update]: {default: PeerAction.HighToleranceError}, }; +function rejectPeerAction(type: GossipType, code: string): PeerAction { + const rule = gossipRejectPeerAction[type]; + return rule.byCode?.[code] ?? rule.default; +} + /** * Similar to getGossipValidatorFn but return a function to accept a batch of beacon_attestation messages * with the same attestation data @@ -97,17 +123,19 @@ export function getGossipValidatorBatchFn( // only beacon_attestation topic is validated in batch metrics?.networkProcessor.gossipAttestationIgnoreByReason.inc({reason: e.type.code}); return TopicValidatorResult.Ignore; - case GossipAction.REJECT: + case GossipAction.REJECT: { metrics?.networkProcessor.gossipValidationReject.inc({topic: type}); // only beacon_attestation topic is validated in batch metrics?.networkProcessor.gossipAttestationRejectByReason.inc({reason: e.type.code}); - core.reportPeer(propagationSource, gossipRejectPeerAction[type], e.type.code); + const peerAction = rejectPeerAction(type, e.type.code); + core.reportPeer(propagationSource, peerAction, e.type.code); logger.debug( `Gossip validation ${type} rejected`, - {peer: propagationSource, clientAgent, clientVersion}, + {peer: propagationSource, clientAgent, clientVersion, peerAction}, e ); return TopicValidatorResult.Reject; + } } }); } catch (e) { @@ -184,11 +212,17 @@ export function getGossipValidatorFn(gossipHandlers: GossipHandlers, modules: Va metrics?.networkProcessor.gossipValidationIgnore.inc({topic: type}); return TopicValidatorResult.Ignore; - case GossipAction.REJECT: + case GossipAction.REJECT: { metrics?.networkProcessor.gossipValidationReject.inc({topic: type}); - core.reportPeer(propagationSource, gossipRejectPeerAction[type], e.type.code); - logger.debug(`Gossip validation ${type} rejected`, {peer: propagationSource, clientAgent, clientVersion}, e); + const peerAction = rejectPeerAction(type, e.type.code); + core.reportPeer(propagationSource, peerAction, e.type.code); + logger.debug( + `Gossip validation ${type} rejected`, + {peer: propagationSource, clientAgent, clientVersion, peerAction}, + e + ); return TopicValidatorResult.Reject; + } } } }; From e542c37f62f7c04f03ba7e124ccf3d7c256a27e5 Mon Sep 17 00:00:00 2001 From: twoeths Date: Thu, 17 Sep 2026 09:54:57 +0700 Subject: [PATCH 4/5] fix: add INVALID_SIGNATURE code for slashing topics --- .../beacon-node/src/chain/errors/attesterSlashingError.ts | 4 +++- .../beacon-node/src/chain/errors/proposerSlashingError.ts | 4 +++- packages/beacon-node/src/chain/validation/attesterSlashing.ts | 3 +-- packages/beacon-node/src/chain/validation/proposerSlashing.ts | 3 +-- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/beacon-node/src/chain/errors/attesterSlashingError.ts b/packages/beacon-node/src/chain/errors/attesterSlashingError.ts index d1201f9e0837..efe0452081da 100644 --- a/packages/beacon-node/src/chain/errors/attesterSlashingError.ts +++ b/packages/beacon-node/src/chain/errors/attesterSlashingError.ts @@ -3,9 +3,11 @@ import {GossipActionError} from "./gossipValidation.js"; export enum AttesterSlashingErrorCode { ALREADY_EXISTS = "ATTESTATION_SLASHING_ERROR_ALREADY_EXISTS", INVALID = "ATTESTATION_SLASHING_ERROR_INVALID", + INVALID_SIGNATURE = "ATTESTATION_SLASHING_ERROR_INVALID_SIGNATURE", } export type AttesterSlashingErrorType = | {code: AttesterSlashingErrorCode.ALREADY_EXISTS} - | {code: AttesterSlashingErrorCode.INVALID; error: Error}; + | {code: AttesterSlashingErrorCode.INVALID; error: Error} + | {code: AttesterSlashingErrorCode.INVALID_SIGNATURE}; export class AttesterSlashingError extends GossipActionError {} diff --git a/packages/beacon-node/src/chain/errors/proposerSlashingError.ts b/packages/beacon-node/src/chain/errors/proposerSlashingError.ts index 2ded67065c3f..29f1d4287c5f 100644 --- a/packages/beacon-node/src/chain/errors/proposerSlashingError.ts +++ b/packages/beacon-node/src/chain/errors/proposerSlashingError.ts @@ -3,9 +3,11 @@ import {GossipActionError} from "./gossipValidation.js"; export enum ProposerSlashingErrorCode { ALREADY_EXISTS = "PROPOSER_SLASHING_ERROR_ALREADY_EXISTS", INVALID = "PROPOSER_SLASHING_ERROR_INVALID", + INVALID_SIGNATURE = "PROPOSER_SLASHING_ERROR_INVALID_SIGNATURE", } export type ProposerSlashingErrorType = | {code: ProposerSlashingErrorCode.ALREADY_EXISTS} - | {code: ProposerSlashingErrorCode.INVALID; error: Error}; + | {code: ProposerSlashingErrorCode.INVALID; error: Error} + | {code: ProposerSlashingErrorCode.INVALID_SIGNATURE}; export class ProposerSlashingError extends GossipActionError {} diff --git a/packages/beacon-node/src/chain/validation/attesterSlashing.ts b/packages/beacon-node/src/chain/validation/attesterSlashing.ts index aec817f24b2f..aed2f1ed09bc 100644 --- a/packages/beacon-node/src/chain/validation/attesterSlashing.ts +++ b/packages/beacon-node/src/chain/validation/attesterSlashing.ts @@ -63,8 +63,7 @@ export async function validateAttesterSlashing( const signatureSets = getAttesterSlashingSignatureSets(chain.config, state.slot, attesterSlashing); if (!(await chain.bls.verifySignatureSets(signatureSets, {batchable: true, priority: prioritizeBls}))) { throw new AttesterSlashingError(GossipAction.REJECT, { - code: AttesterSlashingErrorCode.INVALID, - error: Error("Invalid signature"), + code: AttesterSlashingErrorCode.INVALID_SIGNATURE, }); } } diff --git a/packages/beacon-node/src/chain/validation/proposerSlashing.ts b/packages/beacon-node/src/chain/validation/proposerSlashing.ts index b7f036bea02e..382b12da48c5 100644 --- a/packages/beacon-node/src/chain/validation/proposerSlashing.ts +++ b/packages/beacon-node/src/chain/validation/proposerSlashing.ts @@ -48,8 +48,7 @@ async function validateProposerSlashing( const signatureSets = getProposerSlashingSignatureSets(chain.config, state.slot, proposerSlashing); if (!(await chain.bls.verifySignatureSets(signatureSets, {batchable: true, priority: prioritizeBls}))) { throw new ProposerSlashingError(GossipAction.REJECT, { - code: ProposerSlashingErrorCode.INVALID, - error: Error("Invalid signature"), + code: ProposerSlashingErrorCode.INVALID_SIGNATURE, }); } } From ffee658d3fdb1c856d1dadad739c003daecab063 Mon Sep 17 00:00:00 2001 From: twoeths Date: Thu, 17 Sep 2026 09:56:00 +0700 Subject: [PATCH 5/5] fix: FATAL for every INVALID_SIGNATURE code --- .../network/processor/gossipValidatorFn.ts | 74 +++++++++++++++---- 1 file changed, 59 insertions(+), 15 deletions(-) diff --git a/packages/beacon-node/src/network/processor/gossipValidatorFn.ts b/packages/beacon-node/src/network/processor/gossipValidatorFn.ts index 8b7bcba82304..d0fa9dae8ba6 100644 --- a/packages/beacon-node/src/network/processor/gossipValidatorFn.ts +++ b/packages/beacon-node/src/network/processor/gossipValidatorFn.ts @@ -3,11 +3,20 @@ import {ChainForkConfig} from "@lodestar/config"; import {Logger} from "@lodestar/utils"; import { AttestationError, + AttestationErrorCode, + AttesterSlashingErrorCode, + BlobSidecarErrorCode, + BlockErrorCode, BlsToExecutionChangeErrorCode, + DataColumnSidecarErrorCode, ExecutionPayloadBidErrorCode, + ExecutionPayloadEnvelopeErrorCode, GossipAction, GossipActionError, + PayloadAttestationErrorCode, ProposerPreferencesErrorCode, + ProposerSlashingErrorCode, + SyncCommitteeErrorCode, VoluntaryExitErrorCode, } from "../../chain/errors/index.js"; import {Metrics} from "../../metrics/index.js"; @@ -35,35 +44,70 @@ type RejectPeerActionRule = {default: PeerAction; byCode?: Record = { - [GossipType.beacon_block]: {default: PeerAction.LowToleranceError}, - [GossipType.blob_sidecar]: {default: PeerAction.LowToleranceError}, - [GossipType.data_column_sidecar]: {default: PeerAction.LowToleranceError}, - [GossipType.execution_payload]: {default: PeerAction.LowToleranceError}, - [GossipType.beacon_aggregate_and_proof]: {default: PeerAction.MidToleranceError}, - [GossipType.beacon_attestation]: {default: PeerAction.MidToleranceError}, - [GossipType.sync_committee_contribution_and_proof]: {default: PeerAction.MidToleranceError}, - [GossipType.sync_committee]: {default: PeerAction.MidToleranceError}, - [GossipType.payload_attestation_message]: {default: PeerAction.MidToleranceError}, + [GossipType.beacon_block]: { + default: PeerAction.LowToleranceError, + byCode: {[BlockErrorCode.PROPOSAL_SIGNATURE_INVALID]: PeerAction.Fatal}, + }, + [GossipType.blob_sidecar]: { + default: PeerAction.LowToleranceError, + byCode: {[BlobSidecarErrorCode.PROPOSAL_SIGNATURE_INVALID]: PeerAction.Fatal}, + }, + [GossipType.data_column_sidecar]: { + default: PeerAction.LowToleranceError, + byCode: {[DataColumnSidecarErrorCode.PROPOSAL_SIGNATURE_INVALID]: PeerAction.Fatal}, + }, + [GossipType.execution_payload]: { + default: PeerAction.LowToleranceError, + byCode: {[ExecutionPayloadEnvelopeErrorCode.INVALID_SIGNATURE]: PeerAction.Fatal}, + }, + [GossipType.beacon_aggregate_and_proof]: { + default: PeerAction.MidToleranceError, + byCode: {[AttestationErrorCode.INVALID_SIGNATURE]: PeerAction.Fatal}, + }, + [GossipType.beacon_attestation]: { + default: PeerAction.MidToleranceError, + byCode: {[AttestationErrorCode.INVALID_SIGNATURE]: PeerAction.Fatal}, + }, + [GossipType.sync_committee_contribution_and_proof]: { + default: PeerAction.MidToleranceError, + byCode: {[SyncCommitteeErrorCode.INVALID_SIGNATURE]: PeerAction.Fatal}, + }, + [GossipType.sync_committee]: { + default: PeerAction.MidToleranceError, + byCode: {[SyncCommitteeErrorCode.INVALID_SIGNATURE]: PeerAction.Fatal}, + }, + [GossipType.payload_attestation_message]: { + default: PeerAction.MidToleranceError, + byCode: {[PayloadAttestationErrorCode.INVALID_SIGNATURE]: PeerAction.Fatal}, + }, [GossipType.execution_payload_bid]: { default: PeerAction.HighToleranceError, - byCode: {[ExecutionPayloadBidErrorCode.INVALID_SIGNATURE]: PeerAction.MidToleranceError}, + byCode: {[ExecutionPayloadBidErrorCode.INVALID_SIGNATURE]: PeerAction.Fatal}, }, [GossipType.proposer_preferences]: { default: PeerAction.HighToleranceError, - byCode: {[ProposerPreferencesErrorCode.INVALID_SIGNATURE]: PeerAction.MidToleranceError}, + byCode: {[ProposerPreferencesErrorCode.INVALID_SIGNATURE]: PeerAction.Fatal}, }, [GossipType.voluntary_exit]: { default: PeerAction.HighToleranceError, - byCode: {[VoluntaryExitErrorCode.INVALID_SIGNATURE]: PeerAction.MidToleranceError}, + byCode: {[VoluntaryExitErrorCode.INVALID_SIGNATURE]: PeerAction.Fatal}, }, [GossipType.bls_to_execution_change]: { default: PeerAction.HighToleranceError, - byCode: {[BlsToExecutionChangeErrorCode.INVALID_SIGNATURE]: PeerAction.MidToleranceError}, + byCode: {[BlsToExecutionChangeErrorCode.INVALID_SIGNATURE]: PeerAction.Fatal}, + }, + [GossipType.proposer_slashing]: { + default: PeerAction.HighToleranceError, + byCode: {[ProposerSlashingErrorCode.INVALID_SIGNATURE]: PeerAction.Fatal}, + }, + [GossipType.attester_slashing]: { + default: PeerAction.HighToleranceError, + byCode: {[AttesterSlashingErrorCode.INVALID_SIGNATURE]: PeerAction.Fatal}, }, - [GossipType.proposer_slashing]: {default: PeerAction.HighToleranceError}, - [GossipType.attester_slashing]: {default: PeerAction.HighToleranceError}, + // light client validators only throw IGNORE [GossipType.light_client_finality_update]: {default: PeerAction.HighToleranceError}, [GossipType.light_client_optimistic_update]: {default: PeerAction.HighToleranceError}, };