Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 16 additions & 7 deletions packages/beacon-node/src/api/impl/beacon/blocks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,9 @@ export function getBeaconBlockApi({
throw e;
}),
];
const sentPeersArr = await promiseAllMaybeAsync<number | void>(publishPromises);
const sentPeersArr = await promiseAllMaybeAsync<{sentPeers: number; alreadyPublished: boolean} | number | void>(
publishPromises
);

if (isForkPostGloas(fork)) {
// After gloas, data columns are not published with the block but when publishing the execution payload envelope
Expand All @@ -356,10 +358,13 @@ export function getBeaconBlockApi({
// + 1 because we publish to beacon_block first
for (let i = 0; i < dataColumnSidecars.length; i++) {
// + 1 because we publish to beacon_block first
const sentPeers = sentPeersArr[i + 1] as number;
// sent peers could be 0 as we set `allowPublishToZeroTopicPeers=true` in network.publishDataColumnSidecar() api
const {sentPeers, alreadyPublished} = sentPeersArr[i + 1] as {sentPeers: number; alreadyPublished: boolean};
// sent peers could be 0 as we set `allowPublishToZeroTopicPeers=true` in network.publishDataColumnSidecar()
metrics?.dataColumns.sentPeersPerSubnet.observe(sentPeers);
if (sentPeers === 0) {
// A duplicate publish (alreadyPublished=true) means the column is already propagating on the network —
// expected in self-build flows where peers gossip columns back to us before we publish the envelope.
// Only warn when we genuinely failed to reach any peer. See https://github.com/ChainSafe/lodestar/issues/9527.
if (sentPeers === 0 && !alreadyPublished) {
columnsPublishedWithZeroPeers++;
}
}
Expand Down Expand Up @@ -774,7 +779,9 @@ export function getBeaconBlockApi({
() => chain.processExecutionPayload(payloadInput, {validSignature: true}),
];

const publishPromise = promiseAllMaybeAsync<number | void>(publishPromises);
const publishPromise = promiseAllMaybeAsync<{sentPeers: number; alreadyPublished: boolean} | number | void>(
publishPromises
);

chain.emitter.emit(routes.events.EventType.executionPayloadGossip, {
slot,
Expand All @@ -790,9 +797,11 @@ export function getBeaconBlockApi({
let columnsPublishedWithZeroPeers = 0;
// Skip first entry (envelope); the final entry is processExecutionPayload(), which returns void.
for (let i = 0; i < dataColumnSidecars.length; i++) {
const sentPeers = sentPeersArr[i + 1] as number;
const {sentPeers, alreadyPublished} = sentPeersArr[i + 1] as {sentPeers: number; alreadyPublished: boolean};
metrics?.dataColumns.sentPeersPerSubnet.observe(sentPeers);
if (sentPeers === 0) {
// A duplicate publish means the column is already propagating — expected in self-build flows.
// Only warn when we genuinely failed to reach any peer. See https://github.com/ChainSafe/lodestar/issues/9527.
if (sentPeers === 0 && !alreadyPublished) {
columnsPublishedWithZeroPeers++;
}
}
Expand Down
4 changes: 3 additions & 1 deletion packages/beacon-node/src/network/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,9 @@ export interface INetwork extends INetworkCorePublic {
publishBlobSidecar(blobSidecar: deneb.BlobSidecar): Promise<number>;
publishBeaconAggregateAndProof(aggregateAndProof: SignedAggregateAndProof): Promise<number>;
publishBeaconAttestation(attestation: SingleAttestation, subnet: SubnetID): Promise<number>;
publishDataColumnSidecar(dataColumnSideCar: DataColumnSidecar): Promise<number>;
publishDataColumnSidecar(
dataColumnSideCar: DataColumnSidecar
): Promise<{sentPeers: number; alreadyPublished: boolean}>;
publishVoluntaryExit(voluntaryExit: phase0.SignedVoluntaryExit): Promise<number>;
publishBlsToExecutionChange(blsToExecutionChange: capella.SignedBLSToExecutionChange): Promise<number>;
publishProposerSlashing(proposerSlashing: phase0.ProposerSlashing): Promise<number>;
Expand Down
39 changes: 24 additions & 15 deletions packages/beacon-node/src/network/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ import {
} from "./reqresp/utils/collect.js";
import {collectSequentialBlocksInRange} from "./reqresp/utils/collectSequentialBlocksInRange.js";
import {CommitteeSubscription} from "./subnets/index.js";
import {isPublishToZeroPeersError, prettyPrintPeerIdStr} from "./util.js";
import {isPublishDuplicateError, isPublishToZeroPeersError, prettyPrintPeerIdStr} from "./util.js";

type NetworkModules = {
opts: NetworkOptions;
Expand Down Expand Up @@ -366,26 +366,35 @@ export class Network implements INetwork {
});
}

async publishDataColumnSidecar(dataColumnSidecar: DataColumnSidecar): Promise<number> {
async publishDataColumnSidecar(
dataColumnSidecar: DataColumnSidecar
): Promise<{sentPeers: number; alreadyPublished: boolean}> {
const slot = isGloasDataColumnSidecar(dataColumnSidecar)
? dataColumnSidecar.slot
: dataColumnSidecar.signedBlockHeader.message.slot;
const epoch = computeEpochAtSlot(slot);
const boundary = this.config.getForkBoundaryAtEpoch(epoch);

const subnet = computeSubnetForDataColumnSidecar(this.config, dataColumnSidecar);
return this.publishGossip<GossipType.data_column_sidecar>(
{type: GossipType.data_column_sidecar, boundary, subnet},
dataColumnSidecar,
{
ignoreDuplicatePublishError: true,
// we ensure having all topic peers via prioritizePeers() function
// in the worse case, if there is 0 peer on the topic, the overall publish operation could be still a success
// because supernode will rebuild and publish missing data column sidecars for us
// hence we want to track sent peers as 0 instead of an error
allowPublishToZeroTopicPeers: true,
try {
const sentPeers = await this.publishGossip<GossipType.data_column_sidecar>(
{type: GossipType.data_column_sidecar, boundary, subnet},
dataColumnSidecar,
{
// we ensure having all topic peers via prioritizePeers() function
// in the worse case, if there is 0 peer on the topic, the overall publish operation could be still a success
// because supernode will rebuild and publish missing data column sidecars for us
// hence we want to track sent peers as 0 instead of an error
allowPublishToZeroTopicPeers: true,
}
);
return {sentPeers, alreadyPublished: false};
} catch (e) {
if (isPublishDuplicateError(e as Error)) {
return {sentPeers: 0, alreadyPublished: true};
}
);
throw e;
}
}

async publishBeaconAggregateAndProof(aggregateAndProof: SignedAggregateAndProof): Promise<number> {
Expand Down Expand Up @@ -860,8 +869,8 @@ export class Network implements INetwork {
this.core.setTargetGroupCount(count);
};

private onPublishDataColumns = (sidecars: DataColumnSidecar[]): Promise<number[]> => {
return promiseAllMaybeAsync(sidecars.map((sidecar) => () => this.publishDataColumnSidecar(sidecar)));
private onPublishDataColumns = async (sidecars: DataColumnSidecar[]): Promise<void> => {
await promiseAllMaybeAsync(sidecars.map((sidecar) => () => this.publishDataColumnSidecar(sidecar)));
};

private onPublishBlobSidecars = (sidecars: deneb.BlobSidecar[]): Promise<number[]> => {
Expand Down
5 changes: 5 additions & 0 deletions packages/beacon-node/src/network/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,8 @@ export function getConnection(libp2p: Libp2p, peerIdStr: string): Connection | u
export function isPublishToZeroPeersError(e: Error): boolean {
return e.message.includes("PublishError.NoPeersSubscribedToTopic");
}

// https://github.com/libp2p/js-libp2p/blob/f87cba928991736d9646b3e054c367f55cab315c/packages/gossipsub/src/gossipsub.ts#L2076
export function isPublishDuplicateError(e: Error): boolean {
return e.message.includes("PublishError.Duplicate");
}
2 changes: 1 addition & 1 deletion packages/beacon-node/src/util/execution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ export async function getDataColumnSidecarsFromExecution(
const previouslyMissingColumns = input.getMissingSampledColumnMeta().missing;
const sampledColumns = previouslyMissingColumns.map((columnIndex) => dataColumnSidecars[columnIndex]);

// for columns that we already seen, it will be ignored through `ignoreDuplicatePublishError` gossip option
// for columns we have already seen, publishDataColumnSidecar() catches PublishError.Duplicate and marks alreadyPublished=true
emitter.emit(ChainEvent.publishDataColumns, sampledColumns);
// TODO: Can we record dataColumns.sentPeersPerSubnet metric here somehow

Expand Down