From b7ba49abf0a807a772b4777393bc2890b553be72 Mon Sep 17 00:00:00 2001 From: markolazic01 Date: Thu, 11 Jun 2026 00:07:38 +0200 Subject: [PATCH 1/8] fix: make addPayloadEnvelope idempotent on API path --- .../src/api/impl/beacon/blocks/index.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts index 385eeca7e09f..ff7f0d7659d9 100644 --- a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts @@ -742,12 +742,19 @@ export function getBeaconBlockApi({ throw new ApiError(404, `PayloadEnvelopeInput not found for block root ${blockRootHex}`); } - payloadInput.addPayloadEnvelope({ - envelope: signedExecutionPayloadEnvelope, - source: PayloadEnvelopeInputSource.api, - seenTimestampSec, - peerIdStr: undefined, - }); + if (!payloadInput.hasPayloadEnvelope()) { + payloadInput.addPayloadEnvelope({ + envelope: signedExecutionPayloadEnvelope, + source: PayloadEnvelopeInputSource.api, + seenTimestampSec, + peerIdStr: undefined, + }); + } else { + chain.logger.debug("Execution payload envelope already set, skipping duplicate", { + slot, + blockRoot: blockRootHex, + }); + } if (dataColumnSidecars.length > 0) { for (const columnSidecar of dataColumnSidecars) { From aad8ee5337dd2310d060788a4b2bdff614f19894 Mon Sep 17 00:00:00 2001 From: markolazic01 Date: Sat, 13 Jun 2026 10:56:57 +0200 Subject: [PATCH 2/8] feat: hash mismatch check on publishExecutionPayloadEnvelope --- .../src/api/impl/beacon/blocks/index.ts | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts index ff7f0d7659d9..f55aaf86f497 100644 --- a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts @@ -750,10 +750,21 @@ export function getBeaconBlockApi({ peerIdStr: undefined, }); } else { - chain.logger.debug("Execution payload envelope already set, skipping duplicate", { - slot, - blockRoot: blockRootHex, - }); + const existingEnvelope = payloadInput.getPayloadEnvelope(); + const existingBlockHashHex = toRootHex(existingEnvelope.message.payload.blockHash); + if (blockHashHex !== existingBlockHashHex) { + chain.logger.warn("Execution payload envelope already set with a DIFFERENT block hash", { + slot, + blockRoot: blockRootHex, + existingBlockHash: existingBlockHashHex, + newBlockHash: blockHashHex, + }); + } else { + chain.logger.debug("Execution payload envelope already set, skipping duplicate", { + slot, + blockRoot: blockRootHex, + }); + } } if (dataColumnSidecars.length > 0) { From 1dadb75121edde94037438e44f2ee06063fb4392 Mon Sep 17 00:00:00 2001 From: markolazic01 Date: Sat, 13 Jun 2026 11:20:00 +0200 Subject: [PATCH 3/8] feat: catch errors on envelope validation --- .../src/api/impl/beacon/blocks/index.ts | 39 ++++++++++--------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts index f55aaf86f497..184a133f8602 100644 --- a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts @@ -41,7 +41,7 @@ import {ImportBlockOpts} from "../../../../chain/blocks/types.js"; import {verifyBlocksInEpoch} from "../../../../chain/blocks/verifyBlock.js"; import {BeaconChain} from "../../../../chain/chain.js"; import {ChainEvent} from "../../../../chain/emitter.js"; -import {BlockError, BlockErrorCode, BlockGossipError} from "../../../../chain/errors/index.js"; +import {BlockError, BlockErrorCode, BlockGossipError, ExecutionPayloadEnvelopeError, ExecutionPayloadEnvelopeErrorCode} from "../../../../chain/errors/index.js"; import { BlockType, ProduceFullBellatrix, @@ -694,7 +694,26 @@ export function getBeaconBlockApi({ throw new ApiError(400, `Envelope slot ${slot} does not match block slot ${block.slot}`); } - await validateApiExecutionPayloadEnvelope(chain, signedExecutionPayloadEnvelope); + try { + await validateApiExecutionPayloadEnvelope(chain, signedExecutionPayloadEnvelope); + } catch (e) { + if (e instanceof ExecutionPayloadEnvelopeError) { + if (e.type.code === ExecutionPayloadEnvelopeErrorCode.ENVELOPE_ALREADY_KNOWN) { + chain.logger.debug("Execution payload envelope already known, skipping duplicate", { + slot, + blockRoot: blockRootHex, + }); + return; + } + if (e.type.code === ExecutionPayloadEnvelopeErrorCode.BLOCK_HASH_MISMATCH) { + chain.logger.warn("Execution payload envelope block hash mismatch", { + slot, + blockRoot: blockRootHex, + }); + } + } + throw e; + } const isSelfBuild = envelope.builderIndex === BUILDER_INDEX_SELF_BUILD; let dataColumnSidecars: gloas.DataColumnSidecar[] = []; @@ -749,22 +768,6 @@ export function getBeaconBlockApi({ seenTimestampSec, peerIdStr: undefined, }); - } else { - const existingEnvelope = payloadInput.getPayloadEnvelope(); - const existingBlockHashHex = toRootHex(existingEnvelope.message.payload.blockHash); - if (blockHashHex !== existingBlockHashHex) { - chain.logger.warn("Execution payload envelope already set with a DIFFERENT block hash", { - slot, - blockRoot: blockRootHex, - existingBlockHash: existingBlockHashHex, - newBlockHash: blockHashHex, - }); - } else { - chain.logger.debug("Execution payload envelope already set, skipping duplicate", { - slot, - blockRoot: blockRootHex, - }); - } } if (dataColumnSidecars.length > 0) { From a77f1fe5c267a81af14eed1c90bfa41b656df3d9 Mon Sep 17 00:00:00 2001 From: markolazic01 Date: Thu, 18 Jun 2026 11:38:54 +0200 Subject: [PATCH 4/8] feat: idempotent behavior on second envelope receival --- .../src/api/impl/beacon/blocks/index.ts | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts index 184a133f8602..0740b1ce0806 100644 --- a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts @@ -41,7 +41,13 @@ import {ImportBlockOpts} from "../../../../chain/blocks/types.js"; import {verifyBlocksInEpoch} from "../../../../chain/blocks/verifyBlock.js"; import {BeaconChain} from "../../../../chain/chain.js"; import {ChainEvent} from "../../../../chain/emitter.js"; -import {BlockError, BlockErrorCode, BlockGossipError, ExecutionPayloadEnvelopeError, ExecutionPayloadEnvelopeErrorCode} from "../../../../chain/errors/index.js"; +import { + BlockError, + BlockErrorCode, + BlockGossipError, + ExecutionPayloadEnvelopeError, + ExecutionPayloadEnvelopeErrorCode, +} from "../../../../chain/errors/index.js"; import { BlockType, ProduceFullBellatrix, @@ -697,20 +703,28 @@ export function getBeaconBlockApi({ try { await validateApiExecutionPayloadEnvelope(chain, signedExecutionPayloadEnvelope); } catch (e) { - if (e instanceof ExecutionPayloadEnvelopeError) { - if (e.type.code === ExecutionPayloadEnvelopeErrorCode.ENVELOPE_ALREADY_KNOWN) { - chain.logger.debug("Execution payload envelope already known, skipping duplicate", { + // idempotent behavior towards an already received envelope + if ( + e instanceof ExecutionPayloadEnvelopeError && + e.type.code === ExecutionPayloadEnvelopeErrorCode.ENVELOPE_ALREADY_KNOWN + ) { + const existing = chain.seenPayloadEnvelopeInputCache.get(blockRootHex); + const existingEnvelope = existing?.hasPayloadEnvelope() ? existing.getPayloadEnvelope() : undefined; + const existingBlockHashHex = existingEnvelope && toRootHex(existingEnvelope.message.payload.blockHash); + if (existingBlockHashHex !== undefined && existingBlockHashHex !== blockHashHex) { + chain.logger.warn("Execution payload envelope already known with a DIFFERENT block hash", { slot, blockRoot: blockRootHex, + existingBlockHash: existingBlockHashHex, + newBlockHash: blockHashHex, }); - return; - } - if (e.type.code === ExecutionPayloadEnvelopeErrorCode.BLOCK_HASH_MISMATCH) { - chain.logger.warn("Execution payload envelope block hash mismatch", { + } else { + chain.logger.debug("Execution payload envelope already known, skipping duplicate", { slot, blockRoot: blockRootHex, }); } + return; // 200 OK } throw e; } From c84353b469f916ff55cedb68feaa7d0a31e0b37e Mon Sep 17 00:00:00 2001 From: markolazic01 Date: Thu, 18 Jun 2026 13:07:01 +0200 Subject: [PATCH 5/8] test: publishExecutionPayloadEnvelope idempotency test --- .../publishExecutionPayloadEnvelope.test.ts | 161 ++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 packages/beacon-node/test/unit/api/impl/beacon/blocks/publishExecutionPayloadEnvelope.test.ts diff --git a/packages/beacon-node/test/unit/api/impl/beacon/blocks/publishExecutionPayloadEnvelope.test.ts b/packages/beacon-node/test/unit/api/impl/beacon/blocks/publishExecutionPayloadEnvelope.test.ts new file mode 100644 index 000000000000..9b8a0ee98bce --- /dev/null +++ b/packages/beacon-node/test/unit/api/impl/beacon/blocks/publishExecutionPayloadEnvelope.test.ts @@ -0,0 +1,161 @@ +import {afterEach, beforeEach, describe, expect, it, vi} from "vitest"; +import {when} from "vitest-when"; +import {createChainForkConfig, defaultChainConfig} from "@lodestar/config"; +import {PayloadStatus} from "@lodestar/fork-choice"; +import {ssz} from "@lodestar/types"; +import {toRootHex} from "@lodestar/utils"; +import {getBeaconBlockApi} from "../../../../../../src/api/impl/beacon/blocks/index.js"; +import { + ExecutionPayloadEnvelopeError, + ExecutionPayloadEnvelopeErrorCode, + GossipAction, +} from "../../../../../../src/chain/errors/index.js"; +import {validateApiExecutionPayloadEnvelope} from "../../../../../../src/chain/validation/executionPayloadEnvelope.js"; +import {ApiTestModules, getApiTestModules} from "../../../../../utils/api.js"; +import {generateProtoBlock} from "../../../../../utils/typeGenerator.js"; + +vi.mock("../../../../../../src/chain/validation/executionPayloadEnvelope.js", async (importActual) => { + const mod = await importActual(); + return {...mod, validateApiExecutionPayloadEnvelope: vi.fn()}; +}); + +const gloasConfig = createChainForkConfig({ + ...defaultChainConfig, + GLOAS_FORK_EPOCH: 0, +}); + +describe("api - beacon - publishExecutionPayloadEnvelope", () => { + let modules: ApiTestModules; + let api: ReturnType; + + const slot = 1; + const blockRoot = Buffer.alloc(32, 1); + const blockHash = Buffer.alloc(32, 2); + const blockRootHex = toRootHex(blockRoot); + const blockHashHex = toRootHex(blockHash); + + function buildEnvelope(beaconBlockRoot: Uint8Array, payloadBlockHash: Uint8Array) { + const signed = ssz.gloas.SignedExecutionPayloadEnvelope.defaultValue(); + signed.message.beaconBlockRoot = beaconBlockRoot; + signed.message.payload.slotNumber = slot; + signed.message.payload.blockHash = payloadBlockHash; + return signed; + } + + const signedEnvelope = buildEnvelope(blockRoot, blockHash); + + function makeInput(opts: {hasEnvelope: boolean; storedEnvelope?: ReturnType}) { + return { + hasPayloadEnvelope: vi.fn().mockReturnValue(opts.hasEnvelope), + getPayloadEnvelope: vi.fn().mockReturnValue(opts.storedEnvelope ?? signedEnvelope), + addPayloadEnvelope: vi.fn(), + addColumn: vi.fn(), + }; + } + + function throwAlreadyKnown(): void { + vi.mocked(validateApiExecutionPayloadEnvelope).mockRejectedValue( + new ExecutionPayloadEnvelopeError(GossipAction.IGNORE, { + code: ExecutionPayloadEnvelopeErrorCode.ENVELOPE_ALREADY_KNOWN, + blockRoot: blockRootHex, + slot, + }) + ); + } + + beforeEach(() => { + modules = getApiTestModules({config: gloasConfig}); + api = getBeaconBlockApi(modules); + + when(modules.forkChoice.getBlockHex) + .calledWith(blockRootHex, PayloadStatus.EMPTY) + .thenReturn(generateProtoBlock({slot})); + + (modules.network as any).publishSignedExecutionPayloadEnvelope = vi.fn().mockResolvedValue(1); + modules.chain.processExecutionPayload = vi.fn().mockResolvedValue(undefined) as any; + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it("happy path: unknown envelope is added and published (200)", async () => { + vi.mocked(validateApiExecutionPayloadEnvelope).mockResolvedValue(undefined); + const input = makeInput({hasEnvelope: false}); + modules.chain.seenPayloadEnvelopeInputCache.get.mockReturnValue(input as any); + + await expect( + api.publishExecutionPayloadEnvelope({signedExecutionPayloadEnvelope: signedEnvelope}) + ).resolves.toBeUndefined(); + + expect(input.addPayloadEnvelope).toHaveBeenCalledTimes(1); + expect(modules.chain.processExecutionPayload).toHaveBeenCalledTimes(1); + }); + + it("VC retry: envelope already in cache → 200, not added again", async () => { + throwAlreadyKnown(); + const input = makeInput({hasEnvelope: true}); // same block hash as incoming + modules.chain.seenPayloadEnvelopeInputCache.get.mockReturnValue(input as any); + + await expect( + api.publishExecutionPayloadEnvelope({signedExecutionPayloadEnvelope: signedEnvelope}) + ).resolves.toBeUndefined(); + + expect(input.addPayloadEnvelope).not.toHaveBeenCalled(); + expect(modules.chain.logger.debug).toHaveBeenCalledWith( + "Execution payload envelope already known, skipping duplicate", + expect.objectContaining({slot, blockRoot: blockRootHex}) + ); + expect(modules.chain.logger.warn).not.toHaveBeenCalled(); + }); + + it("gossip-first: already known, cache pruned → 200 (null-safe lookup)", async () => { + throwAlreadyKnown(); + modules.chain.seenPayloadEnvelopeInputCache.get.mockReturnValue(undefined); // pruned + + await expect( + api.publishExecutionPayloadEnvelope({signedExecutionPayloadEnvelope: signedEnvelope}) + ).resolves.toBeUndefined(); + + expect(modules.chain.logger.debug).toHaveBeenCalledWith( + "Execution payload envelope already known, skipping duplicate", + expect.objectContaining({slot, blockRoot: blockRootHex}) + ); + expect(modules.chain.logger.warn).not.toHaveBeenCalled(); + }); + + it("divergent hash: known envelope with a DIFFERENT block hash → warn + 200", async () => { + throwAlreadyKnown(); + const stored = buildEnvelope(blockRoot, Buffer.alloc(32, 0xff)); + const input = makeInput({hasEnvelope: true, storedEnvelope: stored}); + modules.chain.seenPayloadEnvelopeInputCache.get.mockReturnValue(input as any); + + await expect( + api.publishExecutionPayloadEnvelope({signedExecutionPayloadEnvelope: signedEnvelope}) + ).resolves.toBeUndefined(); + + expect(input.addPayloadEnvelope).not.toHaveBeenCalled(); + expect(modules.chain.logger.warn).toHaveBeenCalledWith( + "Execution payload envelope already known with a DIFFERENT block hash", + expect.objectContaining({ + slot, + blockRoot: blockRootHex, + existingBlockHash: toRootHex(stored.message.payload.blockHash), + newBlockHash: blockHashHex, + }) + ); + }); + + it("race path: envelope set between validation and the guard → not added twice, still published", async () => { + vi.mocked(validateApiExecutionPayloadEnvelope).mockResolvedValue(undefined); + const input = makeInput({hasEnvelope: true}); + modules.chain.seenPayloadEnvelopeInputCache.get.mockReturnValue(input as any); + + await expect( + api.publishExecutionPayloadEnvelope({signedExecutionPayloadEnvelope: signedEnvelope}) + ).resolves.toBeUndefined(); + + expect(input.addPayloadEnvelope).not.toHaveBeenCalled(); + expect(modules.chain.processExecutionPayload).toHaveBeenCalledTimes(1); + }); +}); From 3b9f146c49aa496d61a1038be89bb67ebd7501a1 Mon Sep 17 00:00:00 2001 From: markolazic01 Date: Thu, 18 Jun 2026 13:23:06 +0200 Subject: [PATCH 6/8] test: increase test accuracy through new expect checks --- .../beacon/blocks/publishExecutionPayloadEnvelope.test.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/beacon-node/test/unit/api/impl/beacon/blocks/publishExecutionPayloadEnvelope.test.ts b/packages/beacon-node/test/unit/api/impl/beacon/blocks/publishExecutionPayloadEnvelope.test.ts index 9b8a0ee98bce..629440158a64 100644 --- a/packages/beacon-node/test/unit/api/impl/beacon/blocks/publishExecutionPayloadEnvelope.test.ts +++ b/packages/beacon-node/test/unit/api/impl/beacon/blocks/publishExecutionPayloadEnvelope.test.ts @@ -90,6 +90,7 @@ describe("api - beacon - publishExecutionPayloadEnvelope", () => { expect(input.addPayloadEnvelope).toHaveBeenCalledTimes(1); expect(modules.chain.processExecutionPayload).toHaveBeenCalledTimes(1); + expect(modules.network.publishSignedExecutionPayloadEnvelope).toHaveBeenCalledTimes(1); }); it("VC retry: envelope already in cache → 200, not added again", async () => { @@ -102,6 +103,8 @@ describe("api - beacon - publishExecutionPayloadEnvelope", () => { ).resolves.toBeUndefined(); expect(input.addPayloadEnvelope).not.toHaveBeenCalled(); + expect(modules.chain.processExecutionPayload).not.toHaveBeenCalled(); + expect(modules.network.publishSignedExecutionPayloadEnvelope).not.toHaveBeenCalled(); expect(modules.chain.logger.debug).toHaveBeenCalledWith( "Execution payload envelope already known, skipping duplicate", expect.objectContaining({slot, blockRoot: blockRootHex}) @@ -117,6 +120,8 @@ describe("api - beacon - publishExecutionPayloadEnvelope", () => { api.publishExecutionPayloadEnvelope({signedExecutionPayloadEnvelope: signedEnvelope}) ).resolves.toBeUndefined(); + expect(modules.chain.processExecutionPayload).not.toHaveBeenCalled(); + expect(modules.network.publishSignedExecutionPayloadEnvelope).not.toHaveBeenCalled(); expect(modules.chain.logger.debug).toHaveBeenCalledWith( "Execution payload envelope already known, skipping duplicate", expect.objectContaining({slot, blockRoot: blockRootHex}) @@ -134,6 +139,8 @@ describe("api - beacon - publishExecutionPayloadEnvelope", () => { api.publishExecutionPayloadEnvelope({signedExecutionPayloadEnvelope: signedEnvelope}) ).resolves.toBeUndefined(); + expect(modules.chain.processExecutionPayload).not.toHaveBeenCalled(); + expect(modules.network.publishSignedExecutionPayloadEnvelope).not.toHaveBeenCalled(); expect(input.addPayloadEnvelope).not.toHaveBeenCalled(); expect(modules.chain.logger.warn).toHaveBeenCalledWith( "Execution payload envelope already known with a DIFFERENT block hash", @@ -157,5 +164,6 @@ describe("api - beacon - publishExecutionPayloadEnvelope", () => { expect(input.addPayloadEnvelope).not.toHaveBeenCalled(); expect(modules.chain.processExecutionPayload).toHaveBeenCalledTimes(1); + expect(modules.network.publishSignedExecutionPayloadEnvelope).toHaveBeenCalledTimes(1); }); }); From ccd5067c5cb30c7505e4cbc6e0e5e09b97d4cd41 Mon Sep 17 00:00:00 2001 From: markolazic01 Date: Thu, 18 Jun 2026 13:43:02 +0200 Subject: [PATCH 7/8] test: adds specific error rejection tests --- .../publishExecutionPayloadEnvelope.test.ts | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/packages/beacon-node/test/unit/api/impl/beacon/blocks/publishExecutionPayloadEnvelope.test.ts b/packages/beacon-node/test/unit/api/impl/beacon/blocks/publishExecutionPayloadEnvelope.test.ts index 629440158a64..c1bbf5908bcd 100644 --- a/packages/beacon-node/test/unit/api/impl/beacon/blocks/publishExecutionPayloadEnvelope.test.ts +++ b/packages/beacon-node/test/unit/api/impl/beacon/blocks/publishExecutionPayloadEnvelope.test.ts @@ -166,4 +166,62 @@ describe("api - beacon - publishExecutionPayloadEnvelope", () => { expect(modules.chain.processExecutionPayload).toHaveBeenCalledTimes(1); expect(modules.network.publishSignedExecutionPayloadEnvelope).toHaveBeenCalledTimes(1); }); + + it("propagates a non-ENVELOPE_ALREADY_KNOWN validation error (different code)", async () => { + vi.mocked(validateApiExecutionPayloadEnvelope).mockRejectedValue( + new ExecutionPayloadEnvelopeError(GossipAction.REJECT, { + code: ExecutionPayloadEnvelopeErrorCode.INVALID_SIGNATURE, + }) + ); + const input = makeInput({hasEnvelope: false}); + modules.chain.seenPayloadEnvelopeInputCache.get.mockReturnValue(input as any); + + await expect( + api.publishExecutionPayloadEnvelope({signedExecutionPayloadEnvelope: signedEnvelope}) + ).rejects.toThrow(); + + expect(input.addPayloadEnvelope).not.toHaveBeenCalled(); + expect(modules.chain.processExecutionPayload).not.toHaveBeenCalled(); + expect(modules.network.publishSignedExecutionPayloadEnvelope).not.toHaveBeenCalled(); + }); + + it("propagates a non-envelope validation error (plain Error)", async () => { + vi.mocked(validateApiExecutionPayloadEnvelope).mockRejectedValue(new Error("boom")); + const input = makeInput({hasEnvelope: false}); + modules.chain.seenPayloadEnvelopeInputCache.get.mockReturnValue(input as any); + + await expect( + api.publishExecutionPayloadEnvelope({signedExecutionPayloadEnvelope: signedEnvelope}) + ).rejects.toThrow(); + + expect(input.addPayloadEnvelope).not.toHaveBeenCalled(); + expect(modules.chain.processExecutionPayload).not.toHaveBeenCalled(); + expect(modules.network.publishSignedExecutionPayloadEnvelope).not.toHaveBeenCalled(); + }); + + it("rejects 404 when the block is unknown (before validation)", async () => { + when(modules.forkChoice.getBlockHex).calledWith(blockRootHex, PayloadStatus.EMPTY).thenReturn(null); + + await expect( + api.publishExecutionPayloadEnvelope({signedExecutionPayloadEnvelope: signedEnvelope}) + ).rejects.toMatchObject({statusCode: 404}); + + expect(validateApiExecutionPayloadEnvelope).not.toHaveBeenCalled(); + expect(modules.chain.processExecutionPayload).not.toHaveBeenCalled(); + expect(modules.network.publishSignedExecutionPayloadEnvelope).not.toHaveBeenCalled(); + }); + + it("rejects 400 on slot mismatch (before validation)", async () => { + when(modules.forkChoice.getBlockHex) + .calledWith(blockRootHex, PayloadStatus.EMPTY) + .thenReturn(generateProtoBlock({slot: slot + 1})); + + await expect( + api.publishExecutionPayloadEnvelope({signedExecutionPayloadEnvelope: signedEnvelope}) + ).rejects.toMatchObject({statusCode: 400}); + + expect(validateApiExecutionPayloadEnvelope).not.toHaveBeenCalled(); + expect(modules.chain.processExecutionPayload).not.toHaveBeenCalled(); + expect(modules.network.publishSignedExecutionPayloadEnvelope).not.toHaveBeenCalled(); + }); }); From 99d49a6456ff1b1c16abd5990d262b2658d03da0 Mon Sep 17 00:00:00 2001 From: markolazic01 Date: Fri, 3 Jul 2026 00:10:36 +0200 Subject: [PATCH 8/8] fix: mock typing in publishExecutionPayloadEnvelope test --- .../blocks/publishExecutionPayloadEnvelope.test.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/beacon-node/test/unit/api/impl/beacon/blocks/publishExecutionPayloadEnvelope.test.ts b/packages/beacon-node/test/unit/api/impl/beacon/blocks/publishExecutionPayloadEnvelope.test.ts index c1bbf5908bcd..69ee4bbd1037 100644 --- a/packages/beacon-node/test/unit/api/impl/beacon/blocks/publishExecutionPayloadEnvelope.test.ts +++ b/packages/beacon-node/test/unit/api/impl/beacon/blocks/publishExecutionPayloadEnvelope.test.ts @@ -82,7 +82,7 @@ describe("api - beacon - publishExecutionPayloadEnvelope", () => { it("happy path: unknown envelope is added and published (200)", async () => { vi.mocked(validateApiExecutionPayloadEnvelope).mockResolvedValue(undefined); const input = makeInput({hasEnvelope: false}); - modules.chain.seenPayloadEnvelopeInputCache.get.mockReturnValue(input as any); + vi.mocked(modules.chain.seenPayloadEnvelopeInputCache).get.mockReturnValue(input as any); await expect( api.publishExecutionPayloadEnvelope({signedExecutionPayloadEnvelope: signedEnvelope}) @@ -96,7 +96,7 @@ describe("api - beacon - publishExecutionPayloadEnvelope", () => { it("VC retry: envelope already in cache → 200, not added again", async () => { throwAlreadyKnown(); const input = makeInput({hasEnvelope: true}); // same block hash as incoming - modules.chain.seenPayloadEnvelopeInputCache.get.mockReturnValue(input as any); + vi.mocked(modules.chain.seenPayloadEnvelopeInputCache).get.mockReturnValue(input as any); await expect( api.publishExecutionPayloadEnvelope({signedExecutionPayloadEnvelope: signedEnvelope}) @@ -114,7 +114,7 @@ describe("api - beacon - publishExecutionPayloadEnvelope", () => { it("gossip-first: already known, cache pruned → 200 (null-safe lookup)", async () => { throwAlreadyKnown(); - modules.chain.seenPayloadEnvelopeInputCache.get.mockReturnValue(undefined); // pruned + vi.mocked(modules.chain.seenPayloadEnvelopeInputCache).get.mockReturnValue(undefined); // pruned await expect( api.publishExecutionPayloadEnvelope({signedExecutionPayloadEnvelope: signedEnvelope}) @@ -133,7 +133,7 @@ describe("api - beacon - publishExecutionPayloadEnvelope", () => { throwAlreadyKnown(); const stored = buildEnvelope(blockRoot, Buffer.alloc(32, 0xff)); const input = makeInput({hasEnvelope: true, storedEnvelope: stored}); - modules.chain.seenPayloadEnvelopeInputCache.get.mockReturnValue(input as any); + vi.mocked(modules.chain.seenPayloadEnvelopeInputCache).get.mockReturnValue(input as any); await expect( api.publishExecutionPayloadEnvelope({signedExecutionPayloadEnvelope: signedEnvelope}) @@ -156,7 +156,7 @@ describe("api - beacon - publishExecutionPayloadEnvelope", () => { it("race path: envelope set between validation and the guard → not added twice, still published", async () => { vi.mocked(validateApiExecutionPayloadEnvelope).mockResolvedValue(undefined); const input = makeInput({hasEnvelope: true}); - modules.chain.seenPayloadEnvelopeInputCache.get.mockReturnValue(input as any); + vi.mocked(modules.chain.seenPayloadEnvelopeInputCache).get.mockReturnValue(input as any); await expect( api.publishExecutionPayloadEnvelope({signedExecutionPayloadEnvelope: signedEnvelope}) @@ -174,7 +174,7 @@ describe("api - beacon - publishExecutionPayloadEnvelope", () => { }) ); const input = makeInput({hasEnvelope: false}); - modules.chain.seenPayloadEnvelopeInputCache.get.mockReturnValue(input as any); + vi.mocked(modules.chain.seenPayloadEnvelopeInputCache).get.mockReturnValue(input as any); await expect( api.publishExecutionPayloadEnvelope({signedExecutionPayloadEnvelope: signedEnvelope}) @@ -188,7 +188,7 @@ describe("api - beacon - publishExecutionPayloadEnvelope", () => { it("propagates a non-envelope validation error (plain Error)", async () => { vi.mocked(validateApiExecutionPayloadEnvelope).mockRejectedValue(new Error("boom")); const input = makeInput({hasEnvelope: false}); - modules.chain.seenPayloadEnvelopeInputCache.get.mockReturnValue(input as any); + vi.mocked(modules.chain.seenPayloadEnvelopeInputCache).get.mockReturnValue(input as any); await expect( api.publishExecutionPayloadEnvelope({signedExecutionPayloadEnvelope: signedEnvelope})