From 5bf4f26319d4b522dcb7c08cb2f2dbfc65a8aeb7 Mon Sep 17 00:00:00 2001 From: lodekeeper Date: Thu, 4 Jun 2026 18:00:11 +0000 Subject: [PATCH] fix(beacon-node): publish only subscribed columns from reconstruction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirror the getBlobsV2 path in getDataColumnSidecarsFromExecution and align with Lighthouse/Prysm by publishing only sampled (custody + sampling) columns after matrix recovery, instead of cross-seeding every recovered column to non-subscribed subnets. Per consensus-specs PR #4657, eager fanout for non-custody columns floods the network with duplicates because the sender lacks visibility into which peers already saw the message via the topic mesh. A node custodying 65 columns ends up publishing at least 64 (63 non-custody + 1 missing custody) columns every reconstruction, which is more outbound bandwidth than a supernode. Refs: https://github.com/ethereum/consensus-specs/pull/4657 🤖 Generated with AI assistance --- packages/beacon-node/src/util/dataColumns.ts | 29 ++++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/packages/beacon-node/src/util/dataColumns.ts b/packages/beacon-node/src/util/dataColumns.ts index 98a1aa52ba05..3e79a44639ce 100644 --- a/packages/beacon-node/src/util/dataColumns.ts +++ b/packages/beacon-node/src/util/dataColumns.ts @@ -470,15 +470,17 @@ export async function recoverDataColumnSidecars( return DataColumnReconstructionCode.SuccessLate; } - // Once the node obtains a column through reconstruction, - // the node MUST expose the new column as if it had received it over the network. - // If the node is subscribed to the subnet corresponding to the column, - // it MUST send the reconstructed DataColumnSidecar to its topic mesh neighbors. - // If instead the node is not subscribed to the corresponding subnet, - // it SHOULD still expose the availability of the DataColumnSidecar as part of the gossip emission process. - // After exposing the reconstructed DataColumnSidecar to the network, - // the node MAY delete the DataColumnSidecar if it is not part of the node's custody requirement. - const sidecarsToPublish = []; + // Per consensus-specs PR #4657, only publish reconstructed columns the node is + // subscribed to (custody + sampling). Eagerly cross-seeding non-subscribed + // columns floods the network with duplicates because the sender has no + // visibility into which peers already saw the message via the topic mesh. + // This matches the getBlobsV2 path in `getDataColumnSidecarsFromExecution` and + // aligns with Lighthouse/Prysm. Capture missing sampled indices before adding + // any reconstructed columns so they are not filtered out by the subsequent + // `addColumn` calls. + const missingSampledColumns = new Set(input.getMissingSampledColumnMeta().missing); + const sidecarsReconstructed: DataColumnSidecar[] = []; + const sidecarsToPublish: DataColumnSidecar[] = []; for (const columnSidecar of fullSidecars) { if (!input.hasColumn(columnSidecar.index)) { if (input instanceof PayloadEnvelopeInput) { @@ -501,11 +503,14 @@ export async function recoverDataColumnSidecars( source: BlockInputSource.recovery, }); } - sidecarsToPublish.push(columnSidecar); + sidecarsReconstructed.push(columnSidecar); + if (missingSampledColumns.has(columnSidecar.index)) { + sidecarsToPublish.push(columnSidecar); + } } } - metrics?.peerDas.reconstructedColumns.inc(sidecarsToPublish.length); - metrics?.dataColumns.bySource.inc({source: BlockInputSource.recovery}, sidecarsToPublish.length); + metrics?.peerDas.reconstructedColumns.inc(sidecarsReconstructed.length); + metrics?.dataColumns.bySource.inc({source: BlockInputSource.recovery}, sidecarsReconstructed.length); emitter.emit(ChainEvent.publishDataColumns, sidecarsToPublish); // TODO: Can we record dataColumns.sentPeersPerSubnet metric somehow return DataColumnReconstructionCode.SuccessResolved;