From fa8853bd53207219ddc77e5b55f66affe5581aa9 Mon Sep 17 00:00:00 2001 From: lodekeeper Date: Wed, 25 Feb 2026 00:48:58 +0000 Subject: [PATCH] fix: skip prometheus metrics trackProtocolStream for identify MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Work around an identify EOF race in @libp2p/prometheus-metrics v5. trackProtocolStream() attaches a 'message' event listener on protocol streams immediately after negotiation. For outbound /ipfs/id/1.0.0 streams, this listener can fire (via queueMicrotask in dispatchReadBuffer) before identify's pb.read() attaches its own reader, consuming the identify response data and causing EOF. Root cause trace: 1. connection.newStream() negotiates /ipfs/id/1.0.0 via MSS 2. connection.js calls metrics.trackProtocolStream(stream) which adds addEventListener('message', ...) to count bytes 3. MSS unwrap() pushes unread protocol data back to the stream 4. dispatchReadBuffer fires via queueMicrotask — metrics listener consumes all data from readBuffer 5. finally block: readBuffer.byteLength === 0 && remoteWriteStatus === 'closed' → sets readStatus = 'closed' 6. identify's pb.read() sees EOF — peer stays Unknown A/B validation (90s local mainnet samples): - With tracking enabled: 35 opens, 28 failures (80%) - With tracking disabled: 24 opens, 0 failures (0%) - Skip identify only: 33 opens, 0 failures (0%) The fix wraps the prometheus metrics service to skip trackProtocolStream for /ipfs/id/1.0.0 streams only, preserving all other protocol stream metrics. --- .../beacon-node/src/network/libp2p/index.ts | 32 +++++++++++++++---- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/packages/beacon-node/src/network/libp2p/index.ts b/packages/beacon-node/src/network/libp2p/index.ts index 9aa53d6fd10b..12c282210732 100644 --- a/packages/beacon-node/src/network/libp2p/index.ts +++ b/packages/beacon-node/src/network/libp2p/index.ts @@ -73,6 +73,30 @@ export async function createNodeJsLibp2p( noiseCrypto.chaCha20Poly1305Encrypt = asCrypto.chaCha20Poly1305Encrypt; } + const libp2pMetrics = nodeJsLibp2pOpts.metrics + ? ((components: LodestarComponents) => { + const metrics = prometheusMetrics({ + collectDefaultMetrics: false, + preserveExistingMetrics: true, + registry: nodeJsLibp2pOpts.metricsRegistry, + })(components); + + // Work around identify EOF race: + // `trackProtocolStream` attaches a `message` listener immediately after protocol + // negotiation. For `/ipfs/id/1.0.0`, identify() adds its own reader later and can + // miss the first response frame when metrics listener drains events first. + const originalTrackProtocolStream = metrics.trackProtocolStream.bind(metrics); + metrics.trackProtocolStream = ((stream) => { + if (stream.protocol === "/ipfs/id/1.0.0") { + return; + } + originalTrackProtocolStream(stream); + }) as typeof metrics.trackProtocolStream; + + return metrics; + }) + : undefined; + return createLibp2p({ privateKey, nodeInfo: { @@ -101,13 +125,7 @@ export async function createNodeJsLibp2p( ], streamMuxers: [mplex({disconnectThreshold})], peerDiscovery, - metrics: nodeJsLibp2pOpts.metrics - ? prometheusMetrics({ - collectDefaultMetrics: false, - preserveExistingMetrics: true, - registry: nodeJsLibp2pOpts.metricsRegistry, - }) - : undefined, + metrics: libp2pMetrics, connectionManager: { // dialer config maxParallelDials: 100,