From 6c466ef225da221180f6b2c4db8d7c0d414ac8eb Mon Sep 17 00:00:00 2001 From: twoeths Date: Tue, 9 Jun 2026 09:45:58 +0700 Subject: [PATCH 1/7] fix: dedup BlockInputSync metrics --- packages/beacon-node/src/metrics/metrics/lodestar.ts | 5 +++++ packages/beacon-node/src/sync/types.ts | 9 ++++++++- packages/beacon-node/src/sync/unknownBlock.ts | 8 ++++---- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/packages/beacon-node/src/metrics/metrics/lodestar.ts b/packages/beacon-node/src/metrics/metrics/lodestar.ts index 6b082b7ae09b..54d24b9b72ba 100644 --- a/packages/beacon-node/src/metrics/metrics/lodestar.ts +++ b/packages/beacon-node/src/metrics/metrics/lodestar.ts @@ -610,6 +610,11 @@ export function createLodestarMetrics( help: "The origination source of one of the BlockInputSync triggers", labelNames: ["source"], }), + payloadSource: register.gauge<{source: BlockInputSource}>({ + name: "lodestar_payload_input_sync_source_total", + help: "Count of payload (execution payload envelope) sync triggers, labeled by their source", + labelNames: ["source"], + }), pendingBlocks: register.gauge({ name: "lodestar_sync_unknown_block_pending_blocks_size", help: "Current size of UnknownBlockSync pending blocks cache", diff --git a/packages/beacon-node/src/sync/types.ts b/packages/beacon-node/src/sync/types.ts index b79a8e40e91a..fea38f9be1d8 100644 --- a/packages/beacon-node/src/sync/types.ts +++ b/packages/beacon-node/src/sync/types.ts @@ -19,7 +19,14 @@ export enum PendingBlockType { */ INCOMPLETE_BLOCK_INPUT = "IncompleteBlockInput", - UNKNOWN_DATA = "unknown_data", + /** + * Payload analog of UNKNOWN_BLOCK_ROOT: we have a beacon block root but not its execution payload envelope. + */ + UNKNOWN_PAYLOAD_BLOCK_ROOT = "unknown_payload_block_root", + /** + * Payload analog of INCOMPLETE_BLOCK_INPUT: we have a partial payload input that did not complete in time. + */ + INCOMPLETE_PAYLOAD_ENVELOPE = "incomplete_payload_envelope", } export enum PendingBlockInputStatus { diff --git a/packages/beacon-node/src/sync/unknownBlock.ts b/packages/beacon-node/src/sync/unknownBlock.ts index 22c8c10bd3ae..d73ac3a8bf13 100644 --- a/packages/beacon-node/src/sync/unknownBlock.ts +++ b/packages/beacon-node/src/sync/unknownBlock.ts @@ -217,8 +217,8 @@ export class BlockInputSync { try { this.addByPayloadRootHex(data.rootHex, data.peer); this.triggerUnknownBlockSearch(); - this.metrics?.blockInputSync.requests.inc({type: PendingBlockType.UNKNOWN_DATA}); - this.metrics?.blockInputSync.source.inc({source: data.source}); + this.metrics?.blockInputSync.requests.inc({type: PendingBlockType.UNKNOWN_PAYLOAD_BLOCK_ROOT}); + this.metrics?.blockInputSync.payloadSource.inc({source: data.source}); } catch (e) { this.logger.debug("Error handling unknownEnvelopeBlockRoot event", {}, e as Error); } @@ -228,8 +228,8 @@ export class BlockInputSync { try { this.addByPayloadInput(data.payloadInput, data.peer); this.triggerUnknownBlockSearch(); - this.metrics?.blockInputSync.requests.inc({type: PendingBlockType.UNKNOWN_DATA}); - this.metrics?.blockInputSync.source.inc({source: data.source}); + this.metrics?.blockInputSync.requests.inc({type: PendingBlockType.INCOMPLETE_PAYLOAD_ENVELOPE}); + this.metrics?.blockInputSync.payloadSource.inc({source: data.source}); } catch (e) { this.logger.debug("Error handling incompletePayloadEnvelope event", {}, e as Error); } From 48dca69bd8e9adbf91e0890d9821c865b5a7dcfe Mon Sep 17 00:00:00 2001 From: twoeths Date: Tue, 9 Jun 2026 10:06:38 +0700 Subject: [PATCH 2/7] chore: more log for UnknownBlockInput sync --- packages/beacon-node/src/sync/unknownBlock.ts | 38 ++++++++++++++++--- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/packages/beacon-node/src/sync/unknownBlock.ts b/packages/beacon-node/src/sync/unknownBlock.ts index d73ac3a8bf13..1b95dd3556e2 100644 --- a/packages/beacon-node/src/sync/unknownBlock.ts +++ b/packages/beacon-node/src/sync/unknownBlock.ts @@ -331,7 +331,10 @@ export class BlockInputSync { }; this.pendingBlocks.set(blockInput.blockRootHex, pendingBlock); - this.logger.verbose("Added blockInput to BlockInputSync.pendingBlocks", pendingBlock.blockInput.getLogMeta()); + this.logger.verbose("Added blockInput to BlockInputSync.pendingBlocks", { + ...pendingBlock.blockInput.getLogMeta(), + delaySec: this.chain.clock.secFromSlot(blockInput.slot), + }); } if (peerIdStr) { @@ -392,6 +395,13 @@ export class BlockInputSync { } this.pendingPayloads.set(payloadInput.blockRootHex, pendingPayload); + + this.logger.verbose("Added payloadInput to BlockInputSync.pendingPayloads", { + slot: payloadInput.slot, + root: payloadInput.blockRootHex, + delaySec: this.chain.clock.secFromSlot(payloadInput.slot), + }); + const prunedItemCount = pruneSetToMax(this.pendingPayloads, this.maxPendingBlocks); if (prunedItemCount > 0) { this.logger.verbose(`Pruned ${prunedItemCount} items from BlockInputSync.pendingPayloads`); @@ -654,10 +664,12 @@ export class BlockInputSync { } const rootHex = getBlockInputSyncCacheItemRootHex(block); + const blockSlot = getBlockInputSyncCacheItemSlot(block); const logCtx = { - slot: getBlockInputSyncCacheItemSlot(block), + slot: blockSlot, root: rootHex, pendingBlocks: this.pendingBlocks.size, + ...(typeof blockSlot === "number" && {delaySec: this.chain.clock.secFromSlot(blockSlot)}), }; this.logger.verbose("BlockInputSync.downloadBlock()", logCtx); @@ -679,6 +691,7 @@ export class BlockInputSync { const logCtx2 = { ...logCtx, slot: blockSlot, + delaySec, parentInForkChoice, }; this.logger.verbose("Downloaded unknown block", logCtx2); @@ -748,6 +761,12 @@ export class BlockInputSync { // this prevents unbundling attack // see https://lighthouse-blog.sigmaprime.io/mev-unbundling-rpc.html const {slot: blockSlot, proposerIndex} = pendingBlock.blockInput.getBlock().message; + const logCtx = { + slot: blockSlot, + root: pendingBlock.blockInput.blockRootHex, + delaySec: this.chain.clock.secFromSlot(blockSlot), + }; + this.logger.verbose("Processing downloaded block", logCtx); const fork = this.config.getForkName(blockSlot); const proposerBoostWindowMs = this.config.getAttestationDueMs(fork); if ( @@ -783,6 +802,7 @@ export class BlockInputSync { else this.metrics?.blockInputSync.processedBlocksSuccess.inc(); if (!res.err) { + this.logger.verbose("Processed block from unknown sync", logCtx); // no need to update status to "processed", delete anyway this.pendingBlocks.delete(pendingBlock.blockInput.blockRootHex); // Re-enter the scheduler so descendants blocked on either parent blocks or parent payloads @@ -922,10 +942,12 @@ export class BlockInputSync { return; } + const payloadSlot = getPayloadSyncCacheItemSlot(payload); const logCtx = { - slot: getPayloadSyncCacheItemSlot(payload), + slot: payloadSlot, root: rootHex, pendingPayloads: this.pendingPayloads.size, + ...(typeof payloadSlot === "number" && {delaySec: this.chain.clock.secFromSlot(payloadSlot)}), }; this.logger.verbose("BlockInputSync.downloadPayload()", logCtx); @@ -953,7 +975,11 @@ export class BlockInputSync { private async processPayload(pendingPayload: PendingPayloadInput): Promise { const rootHex = pendingPayload.payloadInput.blockRootHex; - const logCtx = {slot: pendingPayload.payloadInput.slot, root: rootHex}; + const logCtx = { + slot: pendingPayload.payloadInput.slot, + root: rootHex, + delaySec: this.chain.clock.secFromSlot(pendingPayload.payloadInput.slot), + }; if (pendingPayload.status !== PendingPayloadInputStatus.downloaded) { this.logger.debug("Skipping payload processing before payload input is downloaded", { @@ -980,6 +1006,7 @@ export class BlockInputSync { } pendingPayload.status = PendingPayloadInputStatus.processing; + this.logger.debug("Processing downloaded payload", logCtx); const res = await wrapError(this.chain.processExecutionPayload(pendingPayload.payloadInput)); if (!res.err) { @@ -1117,6 +1144,7 @@ export class BlockInputSync { rootHex, peerId, peerClient, + ...(typeof slot === "number" && {delaySec: this.chain.clock.secFromSlot(slot)}), hasPayload: pendingPayload.payloadInput.hasPayloadEnvelope(), hasAllData: pendingPayload.payloadInput.hasAllData(), }); @@ -1288,7 +1316,7 @@ export class BlockInputSync { this.metrics?.blockInputSync.fetchBegin.observe(this.chain.clock.secFromSlot(slot, fetchStartSec)); } - const logCtx = {slot, rootHex, peerId, peerClient}; + const logCtx = {slot, rootHex, peerId, peerClient, delaySec: this.chain.clock.secFromSlot(slot)}; this.logger.verbose("BlockInputSync.fetchBlockInput: successful download", logCtx); this.metrics?.blockInputSync.downloadByRoot.success.inc(); const warnings = downloadResult.warnings; From f98eb0d2cb109dadef7a647d3554c1c506edbc1a Mon Sep 17 00:00:00 2001 From: twoeths Date: Tue, 9 Jun 2026 10:21:56 +0700 Subject: [PATCH 3/7] chore: fix logs and metrics of BlockInputSync --- packages/beacon-node/src/chain/emitter.ts | 5 +++-- packages/beacon-node/src/network/processor/index.ts | 2 +- packages/beacon-node/src/sync/types.ts | 4 +++- packages/beacon-node/src/sync/unknownBlock.ts | 9 ++++++--- .../beacon-node/test/e2e/sync/unknownBlockSync.test.ts | 1 + packages/beacon-node/test/unit/sync/unknownBlock.test.ts | 6 ++++++ 6 files changed, 20 insertions(+), 7 deletions(-) diff --git a/packages/beacon-node/src/chain/emitter.ts b/packages/beacon-node/src/chain/emitter.ts index 8a3cd9b45810..9900f8d00c44 100644 --- a/packages/beacon-node/src/chain/emitter.ts +++ b/packages/beacon-node/src/chain/emitter.ts @@ -3,7 +3,7 @@ import {StrictEventEmitter} from "strict-event-emitter-types"; import {routes} from "@lodestar/api"; import {CheckpointWithHex} from "@lodestar/fork-choice"; import {IBeaconStateView} from "@lodestar/state-transition"; -import {DataColumnSidecar, RootHex, deneb, phase0} from "@lodestar/types"; +import {DataColumnSidecar, RootHex, Slot, deneb, phase0} from "@lodestar/types"; import {PeerIdStr} from "../util/peerId.js"; import {BlockInputSource, IBlockInput} from "./blocks/blockInput/types.js"; import {PayloadEnvelopeInput} from "./blocks/payloadEnvelopeInput/payloadEnvelopeInput.js"; @@ -94,7 +94,8 @@ export type ChainEventData = { peer: PeerIdStr; source: BlockInputSource; }; - [ChainEvent.unknownEnvelopeBlockRoot]: {rootHex: RootHex; peer?: PeerIdStr; source: BlockInputSource}; + // slot is the message slot, not necessarily the envelope's slot, but useful as a logging/prune hint + [ChainEvent.unknownEnvelopeBlockRoot]: {rootHex: RootHex; slot: Slot; peer?: PeerIdStr; source: BlockInputSource}; }; export type IChainEvents = ApiEvents & { diff --git a/packages/beacon-node/src/network/processor/index.ts b/packages/beacon-node/src/network/processor/index.ts index 3dd9213b8044..357bb967e48e 100644 --- a/packages/beacon-node/src/network/processor/index.ts +++ b/packages/beacon-node/src/network/processor/index.ts @@ -299,7 +299,7 @@ export class NetworkProcessor { return; } this.unknownEnvelopesBySlot.getOrDefault(slot).add(root); - this.chain.emitter.emit(ChainEvent.unknownEnvelopeBlockRoot, {rootHex: root, peer, source}); + this.chain.emitter.emit(ChainEvent.unknownEnvelopeBlockRoot, {rootHex: root, slot, peer, source}); } private onPendingGossipsubMessage = (message: PendingGossipsubMessage): void => { diff --git a/packages/beacon-node/src/sync/types.ts b/packages/beacon-node/src/sync/types.ts index fea38f9be1d8..030a21aa6ecf 100644 --- a/packages/beacon-node/src/sync/types.ts +++ b/packages/beacon-node/src/sync/types.ts @@ -77,6 +77,8 @@ export type PendingPayloadInput = { export type PendingPayloadRootHex = { status: PendingPayloadInputStatus.pending | PendingPayloadInputStatus.fetching; rootHex: RootHex; + // message slot hint, may be missing when resolving a parent payload + slot?: Slot; timeAddedSec: number; timeSyncedSec?: number; peerIdStrings: Set; @@ -132,5 +134,5 @@ export function getPayloadSyncCacheItemSlot(payload: PayloadSyncCacheItem): Slot return payload.envelope.message.payload.slotNumber; } - return "unknown"; + return payload.slot ?? "unknown"; } diff --git a/packages/beacon-node/src/sync/unknownBlock.ts b/packages/beacon-node/src/sync/unknownBlock.ts index 1b95dd3556e2..4ddcb1f69a71 100644 --- a/packages/beacon-node/src/sync/unknownBlock.ts +++ b/packages/beacon-node/src/sync/unknownBlock.ts @@ -3,7 +3,7 @@ import {ChainForkConfig} from "@lodestar/config"; import {ForkSeq} from "@lodestar/params"; import {RequestError, RequestErrorCode} from "@lodestar/reqresp"; import {computeTimeAtSlot} from "@lodestar/state-transition"; -import {RootHex, gloas} from "@lodestar/types"; +import {RootHex, Slot, gloas} from "@lodestar/types"; import {Logger, fromHex, prettyPrintIndices, pruneSetToMax, sleep, toRootHex} from "@lodestar/utils"; import {isBlockInputBlobs, isBlockInputColumns} from "../chain/blocks/blockInput/blockInput.js"; import {BlockInputSource, IBlockInput} from "../chain/blocks/blockInput/types.js"; @@ -215,7 +215,7 @@ export class BlockInputSync { private onUnknownEnvelopeBlockRoot = (data: ChainEventData[ChainEvent.unknownEnvelopeBlockRoot]): void => { try { - this.addByPayloadRootHex(data.rootHex, data.peer); + this.addByPayloadRootHex(data.rootHex, data.peer, data.slot); this.triggerUnknownBlockSearch(); this.metrics?.blockInputSync.requests.inc({type: PendingBlockType.UNKNOWN_PAYLOAD_BLOCK_ROOT}); this.metrics?.blockInputSync.payloadSource.inc({source: data.source}); @@ -349,13 +349,14 @@ export class BlockInputSync { } }; - private addByPayloadRootHex = (rootHex: RootHex, peerIdStr?: PeerIdStr): boolean => { + private addByPayloadRootHex = (rootHex: RootHex, peerIdStr?: PeerIdStr, slot?: Slot): boolean => { let pendingPayload = this.pendingPayloads.get(rootHex); let added = false; if (!pendingPayload) { pendingPayload = { status: PendingPayloadInputStatus.pending, rootHex, + slot, peerIdStrings: new Set(), timeAddedSec: Date.now() / 1000, }; @@ -363,6 +364,8 @@ export class BlockInputSync { added = true; this.logger.verbose("Added new payload rootHex to BlockInputSync.pendingPayloads", { + slot: slot ?? "unknown", + delaySec: slot != null ? this.chain.clock.secFromSlot(slot) : "unknown", root: rootHex, peerIdStr: peerIdStr ?? "unknown peer", }); diff --git a/packages/beacon-node/test/e2e/sync/unknownBlockSync.test.ts b/packages/beacon-node/test/e2e/sync/unknownBlockSync.test.ts index 7a0479dd0afa..02c36ae4ba51 100644 --- a/packages/beacon-node/test/e2e/sync/unknownBlockSync.test.ts +++ b/packages/beacon-node/test/e2e/sync/unknownBlockSync.test.ts @@ -235,6 +235,7 @@ describe("sync / unknown block sync thru gloas", () => { case ChainEvent.unknownEnvelopeBlockRoot: bn2.chain.emitter.emit(ChainEvent.unknownEnvelopeBlockRoot, { rootHex: headRootHex, + slot: headSlot, peer: sourcePeerId, source: BlockInputSource.gossip, }); diff --git a/packages/beacon-node/test/unit/sync/unknownBlock.test.ts b/packages/beacon-node/test/unit/sync/unknownBlock.test.ts index 3bf7c5dc51af..74bf72a4a13b 100644 --- a/packages/beacon-node/test/unit/sync/unknownBlock.test.ts +++ b/packages/beacon-node/test/unit/sync/unknownBlock.test.ts @@ -761,6 +761,7 @@ describe("UnknownBlockSync", () => { emitter.emit(ChainEvent.unknownEnvelopeBlockRoot, { rootHex: blockRootHex, + slot: 0, peer, source: BlockInputSource.gossip, }); @@ -834,6 +835,7 @@ describe("UnknownBlockSync", () => { emitter.emit(ChainEvent.unknownEnvelopeBlockRoot, { rootHex: blockRootHex, + slot: 0, peer: peerA, source: BlockInputSource.gossip, }); @@ -923,6 +925,7 @@ describe("UnknownBlockSync", () => { emitter.emit(ChainEvent.unknownEnvelopeBlockRoot, { rootHex: blockRootHex, + slot: 0, peer, source: BlockInputSource.gossip, }); @@ -1018,6 +1021,7 @@ describe("UnknownBlockSync", () => { emitter.emit(ChainEvent.unknownEnvelopeBlockRoot, { rootHex: blockRootHex, + slot: 0, peer, source: BlockInputSource.gossip, }); @@ -1074,6 +1078,7 @@ describe("UnknownBlockSync", () => { emitter.emit(ChainEvent.unknownEnvelopeBlockRoot, { rootHex: blockRootHex, + slot: 0, peer, source: BlockInputSource.gossip, }); @@ -1123,6 +1128,7 @@ describe("UnknownBlockSync", () => { emitter.emit(ChainEvent.unknownEnvelopeBlockRoot, { rootHex: blockRootHex, + slot: 0, peer, source: BlockInputSource.gossip, }); From be1b69f2deaf94ac37c80c1bf630b5d26d46e7c1 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Tue, 9 Jun 2026 12:06:28 +0100 Subject: [PATCH 4/7] Update packages/beacon-node/src/metrics/metrics/lodestar.ts Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- packages/beacon-node/src/metrics/metrics/lodestar.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/beacon-node/src/metrics/metrics/lodestar.ts b/packages/beacon-node/src/metrics/metrics/lodestar.ts index 54d24b9b72ba..bd05152f44aa 100644 --- a/packages/beacon-node/src/metrics/metrics/lodestar.ts +++ b/packages/beacon-node/src/metrics/metrics/lodestar.ts @@ -610,7 +610,7 @@ export function createLodestarMetrics( help: "The origination source of one of the BlockInputSync triggers", labelNames: ["source"], }), - payloadSource: register.gauge<{source: BlockInputSource}>({ + payloadSource: register.counter<{source: BlockInputSource}>({ name: "lodestar_payload_input_sync_source_total", help: "Count of payload (execution payload envelope) sync triggers, labeled by their source", labelNames: ["source"], From fda72f7df1a660a5af73d18de0574297c549d5b8 Mon Sep 17 00:00:00 2001 From: Lodekeeper <258435968+lodekeeper@users.noreply.github.com> Date: Tue, 9 Jun 2026 12:43:10 +0100 Subject: [PATCH 5/7] fix(beacon-node): restructure unknownBlock test to fix tsgo overload resolution (#9488) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Restructures the "downloads the block and retries payload import when EL reports block not in fork choice" test in `unknownBlock.test.ts` so its `processBlock` mock no longer needs a `let emitter!: ChainEventEmitter` definite-assignment assertion plus a destructuring-assignment dance. - That pattern caused tsgo's overload resolution to widen `emitter`'s inferred type and fall back to the `EventEmitter` base overload (expecting `unique symbol` event names) for the subsequent `emitter.emit(ChainEvent.unknownEnvelopeBlockRoot, ...)` site — surfaced as the `TS2769` check-types failure on this PR's CI. ## Diagnosis Failing job: `Type Checks (24)` → `packages/beacon-node check-types`. ``` test/unit/sync/unknownBlock.test.ts(1027,20): error TS2769: No overload matches this call. Argument of type 'ChainEvent.unknownEnvelopeBlockRoot' is not assignable to parameter of type 'unique symbol'. ``` The data shape is fine — `slot: 0` is already included at line 1024, and the e2e at `unknownBlockSync.test.ts:236` also includes `slot: headSlot`. The 5 other `emitter.emit(ChainEvent.unknownEnvelopeBlockRoot, ...)` sites in the same file (lines 762/836/926/1079/1129) all typecheck fine because they use plain `const {emitter} = setupPayloadSyncTest(...)` rather than `let !` + destructuring-assignment. ## Fix Declare `processBlock = vi.fn()` upfront, destructure `emitter` as a `const`, then attach `processBlock.mockImplementation(...)` after `setupPayloadSyncTest` returns. This matches the pattern used by every other test in the file and removes the pattern that confused tsgo. Safe because no event fires between `setupPayloadSyncTest` and the next `emitter.emit(...)` — `processBlock` is never invoked before its implementation is attached. ## Test plan - [ ] CI: `Type Checks (24)` passes (no TS2769 on `unknownBlock.test.ts`). - [ ] CI: the "downloads the block and retries payload import when EL reports block not in fork choice" unit test still passes (still asserts `processBlock` called once, `processExecutionPayload` called twice). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: lodekeeper --- .../test/unit/sync/unknownBlock.test.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/beacon-node/test/unit/sync/unknownBlock.test.ts b/packages/beacon-node/test/unit/sync/unknownBlock.test.ts index 74bf72a4a13b..7f6fb56e85ae 100644 --- a/packages/beacon-node/test/unit/sync/unknownBlock.test.ts +++ b/packages/beacon-node/test/unit/sync/unknownBlock.test.ts @@ -963,13 +963,9 @@ describe("UnknownBlockSync", () => { ) .mockResolvedValueOnce(undefined); - let emitter!: ChainEventEmitter; - const processBlock = vi.fn().mockImplementation(async () => { - knownRoots.add(blockRootHex); - emitter.emit(routes.events.EventType.block, {slot: 1, block: blockRootHex, executionOptimistic: false}); - }); + const processBlock = vi.fn(); - ({emitter} = setupPayloadSyncTest({ + const {emitter} = setupPayloadSyncTest({ chainOverrides: { processBlock, processExecutionPayload, @@ -1017,7 +1013,12 @@ describe("UnknownBlockSync", () => { sendBeaconBlocksByRoot, }, peers: [{peerId: peer}], - })); + }); + + processBlock.mockImplementation(async () => { + knownRoots.add(blockRootHex); + emitter.emit(routes.events.EventType.block, {slot: 1, block: blockRootHex, executionOptimistic: false}); + }); emitter.emit(ChainEvent.unknownEnvelopeBlockRoot, { rootHex: blockRootHex, From 9b2aa84736be1be4668f943f6c1c023ea9e69cf8 Mon Sep 17 00:00:00 2001 From: Lodekeeper <258435968+lodekeeper@users.noreply.github.com> Date: Tue, 9 Jun 2026 13:27:03 +0100 Subject: [PATCH 6/7] fix(beacon-node): cast emitter at the failing emit site to fix tsgo overload (#9491) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Follow-up to #9488 — the restructure removed the `let !` + destructuring pattern but tsgo's overload-resolution miss persists at the same emit site because `emitter` is also captured by a sibling closure (the `processBlock` mock body that emits `routes.events.EventType.block`). Same `TS2769` error reproduces on the current head: ``` test/unit/sync/unknownBlock.test.ts(1027,20): error TS2769: No overload matches this call. Argument of type 'ChainEvent.unknownEnvelopeBlockRoot' is not assignable to parameter of type 'unique symbol'. ``` ## Fix Cast `emitter` at the failing site to `ChainEventEmitter` to re-anchor the `StrictEventEmitter` overload for `ChainEvent.X` keys. Minimal change — single line cast at line 1023, with a comment explaining why. The 5 other `emitter.emit(ChainEvent.unknownEnvelopeBlockRoot, ...)` sites in the file remain untouched because they aren't reached through a sibling closure capture. The previous restructure (now in the base branch) is still worth keeping — it removed an unnecessary indirection — but it wasn't sufficient on its own. ## Test plan - [ ] CI: `Type Checks (24)` passes (no TS2769 on `unknownBlock.test.ts`). - [ ] CI: unit tests for the "downloads the block and retries payload import when EL reports block not in fork choice" test still pass — emit semantics unchanged. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: lodekeeper --- packages/beacon-node/test/unit/sync/unknownBlock.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/beacon-node/test/unit/sync/unknownBlock.test.ts b/packages/beacon-node/test/unit/sync/unknownBlock.test.ts index 7f6fb56e85ae..9d45dede3464 100644 --- a/packages/beacon-node/test/unit/sync/unknownBlock.test.ts +++ b/packages/beacon-node/test/unit/sync/unknownBlock.test.ts @@ -1020,7 +1020,9 @@ describe("UnknownBlockSync", () => { emitter.emit(routes.events.EventType.block, {slot: 1, block: blockRootHex, executionOptimistic: false}); }); - emitter.emit(ChainEvent.unknownEnvelopeBlockRoot, { + // tsgo overload-resolution miss when emit is reached through a closure that captures emitter + // first; cast re-anchors the StrictEventEmitter overload for ChainEvent keys. + (emitter as ChainEventEmitter).emit(ChainEvent.unknownEnvelopeBlockRoot, { rootHex: blockRootHex, slot: 0, peer, From fec0c36ff9525820d0d9b3a379aff293d55d2d67 Mon Sep 17 00:00:00 2001 From: Lodekeeper <258435968+lodekeeper@users.noreply.github.com> Date: Tue, 9 Jun 2026 14:48:57 +0100 Subject: [PATCH 7/7] fix(beacon-node): apply tsgo cast + add slot to merged-in #9479 test emit (#9492) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary The base-branch update brought in #9479's new test ("defers envelope validation until the block is in fork choice when payload input is seeded from the block body") which uses the same `let emitter!: ChainEventEmitter` + destructuring-assignment + sibling closure capture pattern that #9491 already mitigated in the original failing test below. Same `TS2769` fires at the new test's emit site: ``` test/unit/sync/unknownBlock.test.ts(1027,20): error TS2769: No overload matches this call. Argument of type 'ChainEvent.unknownEnvelopeBlockRoot' is not assignable to parameter of type 'unique symbol'. ``` Also, #9479's emit predates this PR's addition of `slot: Slot` to the `ChainEvent.unknownEnvelopeBlockRoot` event signature, so the emit data is missing the required `slot` field after the merge. ## Fix Same minimal cast workaround as #9491 plus the required `slot: 0` field: ```diff - emitter.emit(ChainEvent.unknownEnvelopeBlockRoot, { + // tsgo overload-resolution miss when emit is reached through a closure that captures emitter + // first; cast re-anchors the StrictEventEmitter overload for ChainEvent keys (see #9491). + (emitter as ChainEventEmitter).emit(ChainEvent.unknownEnvelopeBlockRoot, { rootHex: blockRootHex, + slot: 0, peer, source: BlockInputSource.gossip, }); ``` Single-site change. The 5 other `emitter.emit(ChainEvent.unknownEnvelopeBlockRoot, ...)` sites in the file remain unchanged because they aren't reached through a sibling closure capture; the cast at the sibling test's emit (introduced in #9491, currently at line 1126) also remains as-is and continues to typecheck cleanly with the expanded `EventType` union from #9439. ## Test plan - [ ] CI: `Type Checks (24)` passes (no TS2769 on `unknownBlock.test.ts`). - [ ] CI: the new `defers envelope validation until the block is in fork choice when payload input is seeded from the block body` test still passes — emit semantics unchanged. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: lodekeeper --- packages/beacon-node/test/unit/sync/unknownBlock.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/beacon-node/test/unit/sync/unknownBlock.test.ts b/packages/beacon-node/test/unit/sync/unknownBlock.test.ts index 379976676fbd..e661ebbd236a 100644 --- a/packages/beacon-node/test/unit/sync/unknownBlock.test.ts +++ b/packages/beacon-node/test/unit/sync/unknownBlock.test.ts @@ -1024,8 +1024,11 @@ describe("UnknownBlockSync", () => { peers: [{peerId: peer}], })); - emitter.emit(ChainEvent.unknownEnvelopeBlockRoot, { + // tsgo overload-resolution miss when emit is reached through a closure that captures emitter + // first; cast re-anchors the StrictEventEmitter overload for ChainEvent keys (see #9491). + (emitter as ChainEventEmitter).emit(ChainEvent.unknownEnvelopeBlockRoot, { rootHex: blockRootHex, + slot: 0, peer, source: BlockInputSource.gossip, });