From 9f460ffae14ebd5dc7d732e0155ed80efaa96b0d Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Mon, 14 Sep 2026 14:52:42 +0100 Subject: [PATCH 1/4] fix: prune cache entries of repeat proposals that are not imported --- .../src/network/processor/gossipHandlers.ts | 16 ++++++++++++++-- .../network/processor/gossipHandlers.test.ts | 14 +++++++++----- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/packages/beacon-node/src/network/processor/gossipHandlers.ts b/packages/beacon-node/src/network/processor/gossipHandlers.ts index 6dfd3fda6925..99bebc440ce6 100644 --- a/packages/beacon-node/src/network/processor/gossipHandlers.ts +++ b/packages/beacon-node/src/network/processor/gossipHandlers.ts @@ -244,8 +244,19 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand // IGNORE means the block is acceptable (e.g. FUTURE_SLOT, ALREADY_KNOWN), just not propagated. // Keep the optimistically-added cache entries; they are pruned on finalization. Only REJECT - // (provably invalid) and unexpected errors prune below. + // (provably invalid), unexpected errors and repeat proposals that are not imported prune. if (e.action === GossipAction.IGNORE) { + // Only a signature-verified sibling is imported by the beacon_block handler, any other repeat proposal + // is dropped from the caches and re-downloaded by sync if it ever becomes relevant + if ( + e.type.code === BlockErrorCode.REPEAT_PROPOSAL && + !chain.seenBlockProposers.hasBlockRoot(slot, signedBlock.message.proposerIndex, blockRootHex) + ) { + chain.seenBlockInputCache.prune(blockRootHex); + if (isForkPostGloas(fork)) { + chain.seenPayloadEnvelopeInputCache.prune(blockRootHex); + } + } throw e; } @@ -738,7 +749,8 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand if ( e instanceof BlockGossipError && e.type.code === BlockErrorCode.REPEAT_PROPOSAL && - // this is make sure the block's proposer signature was verified, it should be true anyway + // Only a signature-verified sibling recorded in the seen cache is imported. The cache holds at most two + // roots per proposer and slot, which bounds full imports over gossip to one alternate chain.seenBlockProposers.hasBlockRoot(signedBlock.message.slot, e.type.proposerIndex, e.type.root) ) { // blockInput was optimistically seeded in validateBeaconBlock and retained on IGNORE diff --git a/packages/beacon-node/test/unit/network/processor/gossipHandlers.test.ts b/packages/beacon-node/test/unit/network/processor/gossipHandlers.test.ts index bbec2d65d6db..9acc792f235d 100644 --- a/packages/beacon-node/test/unit/network/processor/gossipHandlers.test.ts +++ b/packages/beacon-node/test/unit/network/processor/gossipHandlers.test.ts @@ -65,7 +65,8 @@ describe("getGossipHandlers", () => { }); it("imports a signature-verified REPEAT_PROPOSAL (equivocating) block into fork choice but keeps IGNORE", async () => { - const {processBlock, threw} = await runBeaconBlockRepeatProposal(denebConfig, {recorded: true}); + const {processBlock, threw, pruneBlockInput} = await runBeaconBlockRepeatProposal(denebConfig, {recorded: true}); + expect(pruneBlockInput).not.toHaveBeenCalled(); // imported so LMD-GHOST can weigh it ... expect(processBlock).toHaveBeenCalledOnce(); @@ -74,7 +75,9 @@ describe("getGossipHandlers", () => { }); it("does not import a REPEAT_PROPOSAL block whose root was not recorded (unverified 3rd+ proposal)", async () => { - const {processBlock, threw} = await runBeaconBlockRepeatProposal(denebConfig, {recorded: false}); + const {processBlock, threw, pruneBlockInput} = await runBeaconBlockRepeatProposal(denebConfig, {recorded: false}); + // the block is not kept around, sync re-downloads it if it ever becomes relevant + expect(pruneBlockInput).toHaveBeenCalledOnce(); expect(processBlock).not.toHaveBeenCalled(); expect(threw).toBe(true); @@ -162,7 +165,7 @@ async function runBeaconBlockProcessingError( async function runBeaconBlockRepeatProposal( config: BeaconConfig, {recorded}: {recorded: boolean} -): Promise<{processBlock: ReturnType; threw: boolean}> { +): Promise<{processBlock: ReturnType; threw: boolean; pruneBlockInput: ReturnType}> { const logger = testLogger(); const peerIdStr = "16Uiu2HAmTestGossipPeer" as PeerIdStr; const signedBlock = ssz.deneb.SignedBeaconBlock.defaultValue(); @@ -201,6 +204,7 @@ async function runBeaconBlockRepeatProposal( } const processBlock = vi.fn().mockResolvedValue(undefined); + const pruneBlockInput = vi.fn(); const chain = { clock: new ClockStopped(1), custodyConfig: {sampledColumns: [], custodyColumns: []} as unknown as CustodyConfig, @@ -213,7 +217,7 @@ async function runBeaconBlockRepeatProposal( seenBlockInputCache: { getByBlock: vi.fn().mockReturnValue(blockInput), get: vi.fn().mockReturnValue(blockInput), - prune: vi.fn(), + prune: pruneBlockInput, } as unknown as SeenBlockInput, seenPayloadEnvelopeInputCache: { add: vi.fn(), @@ -257,7 +261,7 @@ async function runBeaconBlockRepeatProposal( await new Promise((resolve) => setTimeout(resolve, 0)); await new Promise((resolve) => setTimeout(resolve, 0)); - return {processBlock, threw}; + return {processBlock, threw, pruneBlockInput}; } function getExecutionBlockError( From 06734980e082ebddc59bd0b526c566ca2cb3478c Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Mon, 14 Sep 2026 21:51:51 +0100 Subject: [PATCH 2/4] fix: verify the proposer signature before retaining unknown parent gossip blocks A block with an unknown parent is decoded, hashed, cached and handed to unknown parent sync before its proposer signature is checked, so a peer without a validator key can make the node retain arbitrary blocks. Move the registry and signature checks ahead of the parent lookup, count a retained unknown parent block as the proposer's proposal for the slot so a second one is an equivocation instead of another retained block, and report peers that send blocks with an invalid signature. --- .../beacon-node/src/chain/validation/block.ts | 33 +++--- .../src/network/processor/gossipHandlers.ts | 16 ++- .../test/unit/chain/validation/block.test.ts | 47 ++++++++ .../network/processor/gossipHandlers.test.ts | 106 +++++++++++++++++- 4 files changed, 183 insertions(+), 19 deletions(-) diff --git a/packages/beacon-node/src/chain/validation/block.ts b/packages/beacon-node/src/chain/validation/block.ts index 5a1f5ba2945a..6a72e15315a0 100644 --- a/packages/beacon-node/src/chain/validation/block.ts +++ b/packages/beacon-node/src/chain/validation/block.ts @@ -90,6 +90,22 @@ export async function validateGossipBlock( throw new BlockGossipError(GossipAction.IGNORE, {code: BlockErrorCode.ALREADY_KNOWN, root: blockRoot}); } + // Authenticate the block before the parent lookup so that only blocks signed by a validator are retained for + // unknown parent sync, a peer without a validator key is penalized for every block it sends + // [REJECT] The proposer index is a valid validator index + if (proposerIndex >= chain.pubkeyCache.size) { + throw new BlockGossipError(GossipAction.REJECT, { + code: BlockErrorCode.UNKNOWN_PROPOSER, + slot: blockSlot, + root: blockRoot, + proposerIndex, + }); + } + + // [REJECT] The proposer signature, signed_beacon_block.signature, is valid with respect to the proposer_index pubkey. + await verifyBlockProposerSignature(chain, signedBlock, blockRoot); + chain.seenBlockProposers.observeBlockRoot(blockSlot, proposerIndex, blockRoot, signedBlockHeader); + // [REJECT] The current finalized_checkpoint is an ancestor of block -- i.e. // get_ancestor(store, block.parent_root, compute_start_slot_at_epoch(store.finalized_checkpoint.epoch)) == store.finalized_checkpoint.root const parentRoot = toRootHex(block.parentRoot); @@ -105,6 +121,8 @@ export async function validateGossipBlock( // descend from the finalized root. // (Non-Lighthouse): Since we prune all blocks non-descendant from finalized checking the `db.block` database won't be useful to guard // against known bad fork blocks, so we throw PARENT_BLOCK_UNKNOWN for cases (1) and (2) + // The block is retained for unknown parent sync, count it as this proposer's proposal for the slot + chain.seenBlockProposers.add(blockSlot, proposerIndex, blockRoot); throw new BlockGossipError(GossipAction.IGNORE, {code: BlockErrorCode.PARENT_BLOCK_UNKNOWN, parentRoot}); } @@ -122,6 +140,7 @@ export async function validateGossipBlock( if (isGloasBeaconBlock(block)) { const parentBlockHashHex = toRootHex(block.body.signedExecutionPayloadBid.message.parentBlockHash); if (chain.forkChoice.getBlockHexAndBlockHash(parentRoot, parentBlockHashHex) === null) { + chain.seenBlockProposers.add(blockSlot, proposerIndex, blockRoot); throw new BlockGossipError(GossipAction.IGNORE, { code: BlockErrorCode.PARENT_PAYLOAD_UNKNOWN, parentRoot, @@ -267,20 +286,6 @@ export async function validateGossipBlock( } } - // [REJECT] The proposer index is a valid validator index - if (proposerIndex >= state.validatorCount) { - throw new BlockGossipError(GossipAction.REJECT, { - code: BlockErrorCode.UNKNOWN_PROPOSER, - slot: blockSlot, - root: blockRoot, - proposerIndex, - }); - } - - // [REJECT] The proposer signature, signed_beacon_block.signature, is valid with respect to the proposer_index pubkey. - await verifyBlockProposerSignature(chain, signedBlock, blockRoot); - chain.seenBlockProposers.observeBlockRoot(blockSlot, proposerIndex, blockRoot, signedBlockHeader); - // [REJECT] The block is proposed by the expected proposer_index for the block's slot in the context of the current // shuffling (defined by parent_root/slot). If the proposer_index cannot immediately be verified against the expected // shuffling, the block MAY be queued for later processing while proposers for the block's branch are calculated -- diff --git a/packages/beacon-node/src/network/processor/gossipHandlers.ts b/packages/beacon-node/src/network/processor/gossipHandlers.ts index 99bebc440ce6..a0660104569c 100644 --- a/packages/beacon-node/src/network/processor/gossipHandlers.ts +++ b/packages/beacon-node/src/network/processor/gossipHandlers.ts @@ -246,11 +246,12 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand // Keep the optimistically-added cache entries; they are pruned on finalization. Only REJECT // (provably invalid), unexpected errors and repeat proposals that are not imported prune. if (e.action === GossipAction.IGNORE) { - // Only a signature-verified sibling is imported by the beacon_block handler, any other repeat proposal - // is dropped from the caches and re-downloaded by sync if it ever becomes relevant + // Only a signature-verified sibling with a known parent is imported by the beacon_block handler, any other + // repeat proposal is dropped from the caches and re-downloaded by sync if it ever becomes relevant if ( e.type.code === BlockErrorCode.REPEAT_PROPOSAL && - !chain.seenBlockProposers.hasBlockRoot(slot, signedBlock.message.proposerIndex, blockRootHex) + (!chain.seenBlockProposers.hasBlockRoot(slot, signedBlock.message.proposerIndex, blockRootHex) || + chain.forkChoice.getBlockHexDefaultStatus(toRootHex(signedBlock.message.parentRoot)) === null) ) { chain.seenBlockInputCache.prune(blockRootHex); if (isForkPostGloas(fork)) { @@ -260,6 +261,11 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand throw e; } + if (e.type.code === BlockErrorCode.PROPOSAL_SIGNATURE_INVALID) { + // An honest peer never forwards a block with an invalid proposer signature + core.reportPeer(peerIdStr, PeerAction.LowToleranceError, "InvalidBlockSignature"); + } + chain.persistInvalidSszValue( forkTypes.SignedBeaconBlock, signedBlock, @@ -751,7 +757,9 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand e.type.code === BlockErrorCode.REPEAT_PROPOSAL && // Only a signature-verified sibling recorded in the seen cache is imported. The cache holds at most two // roots per proposer and slot, which bounds full imports over gossip to one alternate - chain.seenBlockProposers.hasBlockRoot(signedBlock.message.slot, e.type.proposerIndex, e.type.root) + chain.seenBlockProposers.hasBlockRoot(signedBlock.message.slot, e.type.proposerIndex, e.type.root) && + // A sibling with an unknown parent cannot be imported, sync fetches it if its branch becomes relevant + chain.forkChoice.getBlockHexDefaultStatus(toRootHex(signedBlock.message.parentRoot)) !== null ) { // blockInput was optimistically seeded in validateBeaconBlock and retained on IGNORE const blockInput = chain.seenBlockInputCache.get(e.type.root); diff --git a/packages/beacon-node/test/unit/chain/validation/block.test.ts b/packages/beacon-node/test/unit/chain/validation/block.test.ts index 0960268683fe..5b8e5d0881a6 100644 --- a/packages/beacon-node/test/unit/chain/validation/block.test.ts +++ b/packages/beacon-node/test/unit/chain/validation/block.test.ts @@ -278,6 +278,53 @@ describe("gossip block validation", () => { ); }); + describe("authentication before the parent lookup", () => { + it("rejects an unknown parent block with an invalid proposer signature before looking up the parent", async () => { + verifySignature.mockResolvedValue(false); + + await expectRejectedWithLodestarError( + validateGossipBlock(config, chain, job, ForkName.phase0), + BlockErrorCode.PROPOSAL_SIGNATURE_INVALID + ); + expect(verifySignature).toHaveBeenCalledOnce(); + expect(chain.seenBlockProposers.isKnown(clockSlot, proposerIndex)).toBe(false); + expect(regen.getState).not.toHaveBeenCalled(); + expect(regen.getPreState).not.toHaveBeenCalled(); + }); + + it("rejects an unknown parent block from a proposer index outside the registry without verifying", async () => { + (chain as unknown as {pubkeyCache: {size: number}}).pubkeyCache = {size: proposerIndex}; + + await expectRejectedWithLodestarError( + validateGossipBlock(config, chain, job, ForkName.phase0), + BlockErrorCode.UNKNOWN_PROPOSER + ); + expect(verifySignature).not.toHaveBeenCalled(); + }); + + it("retains a signed unknown parent block as the proposer's proposal and drops a second one for the slot", async () => { + const blockRoot = toRootHex( + ssz.phase0.BeaconBlockHeader.hashTreeRoot(signedBlockToSignedHeader(config, job).message) + ); + + await expectRejectedWithLodestarError( + validateGossipBlock(config, chain, job, ForkName.phase0), + BlockErrorCode.PARENT_BLOCK_UNKNOWN + ); + expect(chain.seenBlockProposers.isKnown(clockSlot, proposerIndex)).toBe(true); + expect(chain.seenBlockProposers.hasBlockRoot(clockSlot, proposerIndex, blockRoot)).toBe(true); + + // A second unknown parent block from the same proposer is an equivocation, not another retained block + const sibling: SignedBeaconBlock = {signature, message: {...block, stateRoot: Buffer.alloc(32, 1)}}; + await expectRejectedWithLodestarError( + validateGossipBlock(config, chain, sibling, ForkName.phase0), + BlockErrorCode.REPEAT_PROPOSAL + ); + expect(verifySignature).toHaveBeenCalledTimes(2); + expect(chain.seenBlockProposers.isEquivocating(clockSlot, proposerIndex)).toBe(true); + }); + }); + it("NOT_LATER_THAN_PARENT", async () => { // Return not known for proposed block forkChoice.getBlockHexDefaultStatus.mockReturnValueOnce(null); diff --git a/packages/beacon-node/test/unit/network/processor/gossipHandlers.test.ts b/packages/beacon-node/test/unit/network/processor/gossipHandlers.test.ts index 9acc792f235d..bb9d5971393d 100644 --- a/packages/beacon-node/test/unit/network/processor/gossipHandlers.test.ts +++ b/packages/beacon-node/test/unit/network/processor/gossipHandlers.test.ts @@ -82,8 +82,111 @@ describe("getGossipHandlers", () => { expect(processBlock).not.toHaveBeenCalled(); expect(threw).toBe(true); }); + + it("does not import a signature-verified REPEAT_PROPOSAL block whose parent is unknown", async () => { + const {processBlock, threw, pruneBlockInput} = await runBeaconBlockRepeatProposal(denebConfig, { + recorded: true, + parentKnown: false, + }); + expect(pruneBlockInput).toHaveBeenCalledOnce(); + + expect(processBlock).not.toHaveBeenCalled(); + expect(threw).toBe(true); + }); + + it("reports the gossip peer when the proposer signature is invalid", async () => { + const {core, peerIdStr} = await runBeaconBlockValidationReject( + denebConfig, + BlockErrorCode.PROPOSAL_SIGNATURE_INVALID + ); + + expect(core.reportPeer).toHaveBeenCalledOnce(); + expect(core.reportPeer).toHaveBeenCalledWith(peerIdStr, PeerAction.LowToleranceError, "InvalidBlockSignature"); + }); + + it("does not report the gossip peer for other REJECT codes", async () => { + const {core} = await runBeaconBlockValidationReject(denebConfig, BlockErrorCode.INCORRECT_PROPOSER); + + expect(core.reportPeer).not.toHaveBeenCalled(); + }); }); +async function runBeaconBlockValidationReject( + config: BeaconConfig, + code: BlockErrorCode.PROPOSAL_SIGNATURE_INVALID | BlockErrorCode.INCORRECT_PROPOSER +): Promise<{core: Pick; peerIdStr: PeerIdStr}> { + const logger = testLogger(); + const peerIdStr = "16Uiu2HAmTestGossipPeer" as PeerIdStr; + const signedBlock = ssz.deneb.SignedBeaconBlock.defaultValue(); + signedBlock.message.slot = 1; + const blockRootHex = toRootHex(ssz.deneb.BeaconBlock.hashTreeRoot(signedBlock.message)); + const blockInput = BlockInputBlobs.createFromBlock({ + block: signedBlock, + blockRootHex, + forkName: ForkName.deneb, + daOutOfRange: false, + source: BlockInputSource.gossip, + seenTimestampSec: 0, + peerIdStr, + }); + + vi.mocked(validateGossipBlock).mockRejectedValue( + new BlockGossipError(GossipAction.REJECT, { + code, + slot: signedBlock.message.slot, + root: blockRootHex, + proposerIndex: signedBlock.message.proposerIndex, + }) + ); + + const core = {reportPeer: vi.fn()} as Pick; + const chain = { + clock: new ClockStopped(1), + custodyConfig: {sampledColumns: [], custodyColumns: []} as unknown as CustodyConfig, + emitter: new ChainEventEmitter(), + logger, + persistInvalidSszValue: vi.fn(), + processProposerEquivocation: vi.fn(), + seenBlockProposers: new SeenBlockProposers(), + seenBlockInputCache: { + getByBlock: vi.fn().mockReturnValue(blockInput), + prune: vi.fn(), + } as unknown as SeenBlockInput, + seenPayloadEnvelopeInputCache: { + add: vi.fn(), + prune: vi.fn(), + } as unknown as IBeaconChain["seenPayloadEnvelopeInputCache"], + } as unknown as IBeaconChain; + + const handlers = getGossipHandlers( + { + aggregatorTracker: {} as AggregatorTracker, + chain, + config, + core: core as INetworkCore, + events: new NetworkEventBus(), + logger, + metrics: null, + }, + {} + ); + const beaconBlockHandler = handlers[GossipType.beacon_block] as SequentialGossipHandler; + + await expect( + beaconBlockHandler({ + gossipData: {serializedData: ssz.deneb.SignedBeaconBlock.serialize(signedBlock)}, + peerIdStr, + seenTimestampSec: 0, + topic: { + boundary: {fork: ForkName.deneb, epoch: 0}, + type: GossipType.beacon_block, + }, + }) + ).rejects.toThrow(); + + return {core, peerIdStr}; +} + async function runBeaconBlockProcessingError( config: BeaconConfig, code: BlockErrorCode.EXECUTION_ENGINE_ERROR | BlockErrorCode.EXECUTION_ENGINE_INVALID @@ -164,7 +267,7 @@ async function runBeaconBlockProcessingError( async function runBeaconBlockRepeatProposal( config: BeaconConfig, - {recorded}: {recorded: boolean} + {recorded, parentKnown = true}: {recorded: boolean; parentKnown?: boolean} ): Promise<{processBlock: ReturnType; threw: boolean; pruneBlockInput: ReturnType}> { const logger = testLogger(); const peerIdStr = "16Uiu2HAmTestGossipPeer" as PeerIdStr; @@ -209,6 +312,7 @@ async function runBeaconBlockRepeatProposal( clock: new ClockStopped(1), custodyConfig: {sampledColumns: [], custodyColumns: []} as unknown as CustodyConfig, emitter: new ChainEventEmitter(), + forkChoice: {getBlockHexDefaultStatus: vi.fn().mockReturnValue(parentKnown ? {} : null)}, getBlobsTracker: {triggerGetBlobs: vi.fn()}, logger, processBlock, From 7f976f85ee3f85f4d316ea07d3f9ee7568c142b0 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Sun, 20 Sep 2026 11:25:56 +0100 Subject: [PATCH 3/4] leave the invalid signature peer penalty to the gossip validator Since #10059 the validator wrapper reports the peer for every REJECT by topic and code, Fatal for an invalid proposer signature on beacon_block, so the handler-level LowToleranceError report was a second, weaker penalty for the same rejection. --- .../src/network/processor/gossipHandlers.ts | 5 ---- .../network/processor/gossipHandlers.test.ts | 25 ++++++------------- 2 files changed, 7 insertions(+), 23 deletions(-) diff --git a/packages/beacon-node/src/network/processor/gossipHandlers.ts b/packages/beacon-node/src/network/processor/gossipHandlers.ts index 195c34a3ff2d..eee29c17466b 100644 --- a/packages/beacon-node/src/network/processor/gossipHandlers.ts +++ b/packages/beacon-node/src/network/processor/gossipHandlers.ts @@ -262,11 +262,6 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand throw e; } - if (e.type.code === BlockErrorCode.PROPOSAL_SIGNATURE_INVALID) { - // An honest peer never forwards a block with an invalid proposer signature - core.reportPeer(peerIdStr, PeerAction.LowToleranceError, "InvalidBlockSignature"); - } - chain.persistInvalidSszValue( forkTypes.SignedBeaconBlock, signedBlock, diff --git a/packages/beacon-node/test/unit/network/processor/gossipHandlers.test.ts b/packages/beacon-node/test/unit/network/processor/gossipHandlers.test.ts index ddabbdc974ae..ee2c452212ad 100644 --- a/packages/beacon-node/test/unit/network/processor/gossipHandlers.test.ts +++ b/packages/beacon-node/test/unit/network/processor/gossipHandlers.test.ts @@ -110,18 +110,8 @@ describe("getGossipHandlers", () => { expect(threw).toBe(true); }); - it("reports the gossip peer when the proposer signature is invalid", async () => { - const {core, peerIdStr} = await runBeaconBlockValidationReject( - denebConfig, - BlockErrorCode.PROPOSAL_SIGNATURE_INVALID - ); - - expect(core.reportPeer).toHaveBeenCalledOnce(); - expect(core.reportPeer).toHaveBeenCalledWith(peerIdStr, PeerAction.LowToleranceError, "InvalidBlockSignature"); - }); - - it("does not report the gossip peer for other REJECT codes", async () => { - const {core} = await runBeaconBlockValidationReject(denebConfig, BlockErrorCode.INCORRECT_PROPOSER); + it("leaves the peer penalty for a rejected block to the gossip validator", async () => { + const {core} = await runBeaconBlockValidationReject(denebConfig, BlockErrorCode.PROPOSAL_SIGNATURE_INVALID); expect(core.reportPeer).not.toHaveBeenCalled(); }); @@ -129,8 +119,8 @@ describe("getGossipHandlers", () => { async function runBeaconBlockValidationReject( config: BeaconConfig, - code: BlockErrorCode.PROPOSAL_SIGNATURE_INVALID | BlockErrorCode.INCORRECT_PROPOSER -): Promise<{core: Pick; peerIdStr: PeerIdStr}> { + code: BlockErrorCode.PROPOSAL_SIGNATURE_INVALID +): Promise<{core: Pick}> { const logger = testLogger(); const peerIdStr = "16Uiu2HAmTestGossipPeer" as PeerIdStr; const signedBlock = ssz.deneb.SignedBeaconBlock.defaultValue(); @@ -151,7 +141,6 @@ async function runBeaconBlockValidationReject( code, slot: signedBlock.message.slot, root: blockRootHex, - proposerIndex: signedBlock.message.proposerIndex, }) ); @@ -166,11 +155,11 @@ async function runBeaconBlockValidationReject( seenBlockProposers: new SeenBlockProposers(), seenBlockInputCache: { getByBlock: vi.fn().mockReturnValue(blockInput), - prune: vi.fn(), + remove: vi.fn(), } as unknown as SeenBlockInput, seenPayloadEnvelopeInputCache: { add: vi.fn(), - prune: vi.fn(), + remove: vi.fn(), } as unknown as IBeaconChain["seenPayloadEnvelopeInputCache"], } as unknown as IBeaconChain; @@ -200,7 +189,7 @@ async function runBeaconBlockValidationReject( }) ).rejects.toThrow(); - return {core, peerIdStr}; + return {core}; } async function runBeaconBlockProcessingError( From 7e4f88bbd5ce7e03f22f74e35e24e3fca5346da1 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Sun, 20 Sep 2026 11:38:54 +0100 Subject: [PATCH 4/4] say remove where the comment still said prune --- packages/beacon-node/src/network/processor/gossipHandlers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/beacon-node/src/network/processor/gossipHandlers.ts b/packages/beacon-node/src/network/processor/gossipHandlers.ts index eee29c17466b..dcb5f4b72bb8 100644 --- a/packages/beacon-node/src/network/processor/gossipHandlers.ts +++ b/packages/beacon-node/src/network/processor/gossipHandlers.ts @@ -244,7 +244,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand // IGNORE means the block is acceptable (e.g. FUTURE_SLOT, ALREADY_KNOWN), just not propagated. // Keep the optimistically-added cache entries; they are pruned on finalization. Only REJECT - // (provably invalid), unexpected errors and repeat proposals that are not imported prune. + // (provably invalid), unexpected errors and repeat proposals that are not imported remove the entry. if (e.action === GossipAction.IGNORE) { // Only a signature-verified sibling with a known parent is imported by the beacon_block handler, any other // repeat proposal is dropped from the caches and re-downloaded by sync if it ever becomes relevant. Only its