Skip to content
Open
Changes from 1 commit
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
5 changes: 4 additions & 1 deletion packages/beacon-node/src/api/impl/beacon/blocks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,10 @@ export function getBeaconBlockApi({
try {
await validateGossipBlock(config, chain, signedBlock, fork);
} catch (error) {
if (error instanceof BlockGossipError && error.type.code === BlockErrorCode.ALREADY_KNOWN) {
if (
error instanceof BlockGossipError &&
(error.type.code === BlockErrorCode.ALREADY_KNOWN || error.type.code === BlockErrorCode.REPEAT_PROPOSAL)
) {
Comment on lines +196 to +197
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Don't swallow equivocation under REPEAT_PROPOSAL

REPEAT_PROPOSAL is raised for any second proposal by the same proposer/slot, not just benign duplicates of an already-known root. By returning success for this code path, publishBlock() now silently drops blocks that may be a different root (equivocation or failover mismatch) before the publish/import section runs, so the caller receives a false-positive success even though this block was neither broadcast nor processed. Consider only suppressing when the same block root is already known/in-flight, and keep a surfaced error for non-identical repeat proposals.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point — worth discussing the tradeoff.

REPEAT_PROPOSAL does fire purely on (slot, proposerIndex) (seenBlockProposers.ts tracks no root), so the suppression is semantically wider than ALREADY_KNOWN. The cases it collapses:

  1. Same root, race window. API publish arrives after gossip validation has run for the same block (adds to seenBlockProposers at block.ts:248) but before fork-choice import finishes. ALREADY_KNOWN at block.ts:59 returns null; REPEAT_PROPOSAL at block.ts:68 fires. This is the case issue POST eth/v2/beacon/blocks returning BLOCK_ERROR_REPEAT_PROPOSAL in multi-node setup #9228 describes.

  2. Distinct root (equivocation / failover mismatch / VC bug). Same (slot, proposer), different body.

In both cases the block is rejected by gossip validation before the publishBlock/import section — so nothing is broadcast or imported either way. The only observable difference for the caller is 500 vs 2xx.

Reliably distinguishing (1) from (2) would need SeenBlockProposers to track seen roots per (slot, proposer), which is a structural cache change beyond the scope of this targeted fix. The reason: error.type.code metadata now in the log (commit b68ea2b, per @gemini-code-assist suggestion) does surface the REPEAT_PROPOSAL case to operators so case (2) is still observable.

Happy to tighten if @nflaig prefers — options I see:

  • (a) keep current behavior and rely on the reason log field for observability (current PR),
  • (b) extend SeenBlockProposers with a seenRootsByProposer: Map<Slot, Map<ValidatorIndex, Set<RootHex>>> and only suppress when seenRootsByProposer.get(slot)?.get(proposerIndex)?.has(blockRoot) is true, surfacing otherwise,
  • (c) keep current behavior but add a warn-level log for the REPEAT_PROPOSAL branch to make distinct-root re-submissions stand out in logs.

chain.logger.debug("Ignoring known block during publishing", valLogMeta);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The log message "Ignoring known block" is slightly inaccurate when the error is REPEAT_PROPOSAL (which indicates a block for the same slot/proposer was seen, but not necessarily the same block root). Including the actual error code in the log metadata would improve observability and help distinguish between these two cases during debugging.

Suggested change
chain.logger.debug("Ignoring known block during publishing", valLogMeta);
chain.logger.debug("Ignoring block during publishing", {...valLogMeta, reason: error.type.code});

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — applied in b68ea2b. The log is now "Ignoring block during publishing" with reason: error.type.code in metadata, so ALREADY_KNOWN vs REPEAT_PROPOSAL is distinguishable.

// Blocks might already be published by another node as part of a fallback setup or DVT cluster
// and can reach our node by gossip before the api. The error can be ignored and should not result in a 500 response.
Expand Down